diff --git a/core/opentaint-configuration-rules/configuration-rules-common/src/main/kotlin/org/opentaint/dataflow/configuration/ConditionSimplifier.kt b/core/opentaint-configuration-rules/configuration-rules-common/src/main/kotlin/org/opentaint/dataflow/configuration/ConditionSimplifier.kt index 364d9fc6d..8eb5aafaf 100644 --- a/core/opentaint-configuration-rules/configuration-rules-common/src/main/kotlin/org/opentaint/dataflow/configuration/ConditionSimplifier.kt +++ b/core/opentaint-configuration-rules/configuration-rules-common/src/main/kotlin/org/opentaint/dataflow/configuration/ConditionSimplifier.kt @@ -57,6 +57,7 @@ private class ConditionSimplifierImpl : CommonConditionVisitor() +private val falseCondition: CommonCondition = Not(CommonCondition.True) @Suppress("UNCHECKED_CAST") fun conditionSimplifier(): CommonConditionVisitor> = @@ -66,7 +67,9 @@ fun conditionSimplifier(): CommonConditionVisitor> = fun mkTrue(): CommonCondition = CommonCondition.True as CommonCondition -fun mkFalse(): CommonCondition = Not(mkTrue()) +@Suppress("UNCHECKED_CAST") +fun mkFalse(): CommonCondition = + falseCondition as CommonCondition fun mkOr(conditions: List>) = when (conditions.size) { 0 -> mkFalse() diff --git a/core/opentaint-configuration-rules/configuration-rules-common/src/test/kotlin/org/opentaint/dataflow/configuration/ConditionFactoryTest.kt b/core/opentaint-configuration-rules/configuration-rules-common/src/test/kotlin/org/opentaint/dataflow/configuration/ConditionFactoryTest.kt new file mode 100644 index 000000000..1bdbec5ae --- /dev/null +++ b/core/opentaint-configuration-rules/configuration-rules-common/src/test/kotlin/org/opentaint/dataflow/configuration/ConditionFactoryTest.kt @@ -0,0 +1,14 @@ +package org.opentaint.dataflow.configuration + +import kotlin.test.Test +import kotlin.test.assertSame + +class ConditionFactoryTest { + @Test + fun `false condition is shared`() { + val first: Any = mkFalse() + val second: Any = mkFalse() + + assertSame(first, second) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLong2ObjectMap.java b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLong2ObjectMap.java new file mode 100644 index 000000000..288e46c60 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLong2ObjectMap.java @@ -0,0 +1,75 @@ +package org.opentaint.dataflow.util; + +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; + +/** + * A primitive long map with point reads that tolerate a concurrent rehash. + * + *

The supported concurrency model is one writer and any number of readers. Removals are not + * supported. Iteration must use the captured-table helper in {@code MapUtils.kt}; the inherited + * fastutil iterators are not concurrent-read-safe.

+ */ +public final class ConcurrentReadSafeLong2ObjectMap extends Long2ObjectOpenHashMap { + @Override + public @Nullable V get(long k) { + if (k == 0) { + if (!containsNullKey) return defRetValue; + + do { + int n = this.n; + V[] value = this.value; + if (value.length == n + 1) return value[n]; + } while (true); + } + + while (true) { + long[] key = this.key; + V[] value = this.value; + int n = this.n; + + // Capture a matching table generation to allow a read during rehash. + if (key.length != n + 1 || value.length != n + 1) continue; + + int mask = n - 1; + int pos = (int) HashCommon.mix(k) & mask; + long curr = key[pos]; + if (curr == 0) return defRetValue; + + if (k == curr) return value[pos]; + + // There's always an unused entry. + while (true) { + pos = (pos + 1) & mask; + curr = key[pos]; + if (curr == 0) return defRetValue; + + if (k == curr) return value[pos]; + } + } + } + + @Override + public V remove(long k) { + throw new UnsupportedOperationException("Removals are not allowed"); + } + + public long[] getKeys() { + return this.key; + } + + public V[] getValues() { + return this.value; + } + + public int getN() { + return this.n; + } + + public boolean getContainsNullKey() { + return this.containsNullKey; + } + + private static final long serialVersionUID = 0L; +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLongSet.java b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLongSet.java new file mode 100644 index 000000000..ba0aaa1be --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLongSet.java @@ -0,0 +1,61 @@ +package org.opentaint.dataflow.util; + +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + +/** + * A primitive long set with point reads that tolerate a concurrent rehash. + * + *

The supported concurrency model is one writer and any number of readers. Removals are not + * supported. Iteration must use the captured-table helper in {@code MapUtils.kt}; the inherited + * fastutil iterators are not concurrent-read-safe.

+ */ +public final class ConcurrentReadSafeLongSet extends LongOpenHashSet { + @Override + public boolean contains(long k) { + if (k == 0) return containsNull; + + while (true) { + long[] key = this.key; + int n = this.n; + + // Capture one complete table generation to allow a read during rehash. + if (key.length != n + 1) continue; + + int mask = n - 1; + int pos = (int) HashCommon.mix(k) & mask; + long curr = key[pos]; + if (curr == 0) return false; + + if (k == curr) return true; + + // There's always an unused entry. + while (true) { + pos = (pos + 1) & mask; + curr = key[pos]; + if (curr == 0) return false; + + if (k == curr) return true; + } + } + } + + @Override + public boolean remove(long k) { + throw new UnsupportedOperationException("Removals are not allowed"); + } + + public long[] getKeys() { + return this.key; + } + + public int getN() { + return this.n; + } + + public boolean getContainsNull() { + return this.containsNull; + } + + private static final long serialVersionUID = 0L; +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java index edbabe33c..00ecd584d 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java @@ -4,9 +4,18 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import org.jetbrains.annotations.Nullable; +/** + * A flat object-to-int map supporting one writer and multiple concurrent point readers. + * + *

Writes are published through a sequence counter. Readers retry if a write overlaps their + * lookup, which prevents observing a key before its primitive value or a partially published + * rehash. Removals are not supported.

+ */ public final class ConcurrentReadSafeObject2IntMap extends Object2IntOpenHashMap { public static final int NO_VALUE = -1; + private volatile long writeSequence; + public ConcurrentReadSafeObject2IntMap() { super(); defaultReturnValue(NO_VALUE); @@ -14,46 +23,63 @@ public ConcurrentReadSafeObject2IntMap() { @Override public int getInt(@Nullable Object k) { - if (k == null) { - if (!containsNullKey) return defRetValue; - - do { - int n = this.n; - int[] value = this.value; - if (value.length == n + 1) return value[n]; - } while (true); - } - while (true) { + long sequenceBefore = writeSequence; + if ((sequenceBefore & 1) != 0) continue; + K[] key = this.key; int[] value = this.value; - int n = this.n; + int result = findValue(k, key, value); - // capture arrays to allow concurrent reads - if (key.length != n + 1 || value.length != n + 1) continue; + if (sequenceBefore == writeSequence) return result; + } + } - int mask = n - 1; + private int findValue(@Nullable Object k, K[] key, int[] value) { + if (k == null) return containsNullKey ? value[value.length - 1] : defRetValue; - // The starting point. - int pos = HashCommon.mix(k.hashCode()) & mask; + int mask = key.length - 2; + int pos = HashCommon.mix(k.hashCode()) & mask; + K curr = key[pos]; + if (curr == null) return defRetValue; + if (k.equals(curr)) return value[pos]; - K curr = key[pos]; + while (true) { + pos = (pos + 1) & mask; + curr = key[pos]; if (curr == null) return defRetValue; - if (k.equals(curr)) return value[pos]; + } + } - // There's always an unused entry. - while (true) { - pos = (pos + 1) & mask; - - curr = key[pos]; - if (curr == null) return defRetValue; + @Override + public int put(K key, int value) { + beginWrite(); + try { + return super.put(key, value); + } finally { + endWrite(); + } + } - if (k.equals(curr)) return value[pos]; - } + @Override + public int putIfAbsent(K key, int value) { + beginWrite(); + try { + return super.putIfAbsent(key, value); + } finally { + endWrite(); } } + private void beginWrite() { + writeSequence++; + } + + private void endWrite() { + writeSequence++; + } + @Override public int removeInt(Object k) { throw new UnsupportedOperationException("Removals are not allowed"); diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt index 42456f4f0..441026c57 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt @@ -17,6 +17,7 @@ interface AnalysisRunner { val methodCallResolver: MethodCallResolver fun enqueueMethodAnalyzer(analyzer: MethodAnalyzer) + fun reprioritizeMethodAnalyzer(analyzer: MethodAnalyzer) fun registerDelayedAnalyzer(analyzer: MethodAnalyzer) fun addNewSummaryEdges(methodEntryPoint: MethodEntryPoint, edges: List) fun getPrecalculatedSummaries(methodEntryPoint: MethodEntryPoint): Pair, List>? diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisUnitRunnerManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisUnitRunnerManager.kt index e477ee867..b550be8e7 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisUnitRunnerManager.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisUnitRunnerManager.kt @@ -6,14 +6,17 @@ import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ifds.UnitResolver import org.opentaint.dataflow.ifds.UnitType import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager interface AnalysisUnitRunnerManager { val unitResolver: UnitResolver val cancellation: Cancellation + val refManager: RefManager fun getOrCreateUnitStorage(unit: UnitType): MethodSummariesUnitStorage? fun getOrCreateUnitRunner(unit: UnitType): AnalysisRunner? fun registerMethodCallFromUnit(method: CommonMethod, unit: UnitType) + fun registerResolvedMethodCall(caller: CommonMethod, callee: CommonMethod) fun handleCrossUnitZeroCall(callerUnit: UnitType, methodEntryPoint: MethodEntryPoint) { handleCrossUnitAction(callerUnit, methodEntryPoint) { @@ -109,6 +112,15 @@ interface AnalysisUnitRunnerManager { return storage.methodFactToFactSummaryEdges(methodEntryPoint, finalFactBase) } + fun findFactToFactSummaryEdges( + methodEntryPoint: MethodEntryPoint, + finalFactPattern: FinalFactAp, + ): List { + val unit = unitResolver.resolve(methodEntryPoint.method) + val storage = getOrCreateUnitStorage(unit) ?: return emptyList() + return storage.methodFactToFactSummaryEdges(methodEntryPoint, finalFactPattern) + } + fun findFactNDSummaryEdges( methodEntryPoint: MethodEntryPoint, finalFactBase: AccessPathBase diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/EdgeCollection.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/EdgeCollection.kt index 02ea19741..6bed229a6 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/EdgeCollection.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/EdgeCollection.kt @@ -7,6 +7,34 @@ import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.ir.api.common.cfg.CommonInst object EdgeCollection { + class UnprocessedEdgeList( + apManager: ApManager, + methodEntryPoint: MethodEntryPoint, + ) { + private val zeroToZeroEdges = arrayListOf() + private val otherEdges = EdgeList(apManager, methodEntryPoint) + + val containsZeroToZeroEdges: Boolean + get() = zeroToZeroEdges.isNotEmpty() + + val isEmpty: Boolean + get() = zeroToZeroEdges.isEmpty() && otherEdges.isEmpty + + val size: Int + get() = zeroToZeroEdges.size + otherEdges.size + + fun add(edge: Edge) { + if (edge is Edge.ZeroToZero) { + zeroToZeroEdges.add(edge) + } else { + otherEdges.add(edge) + } + } + + fun removeLast(): Edge = + zeroToZeroEdges.removeLastOrNull() ?: otherEdges.removeLast() + } + class EdgeList( private val apManager: ApManager, private val methodEntryPoint: MethodEntryPoint diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt index 30453d1e2..648441945 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt @@ -34,37 +34,48 @@ sealed interface ExclusionSet { override fun toString(): String = "*" } - data class Concrete( - val set: PersistentSet, - private val hash: Int, + class Concrete private constructor( + val set: Set, + @Volatile + private var cachedHash: Int?, ) : ExclusionSet { + constructor(set: PersistentSet) : this(set, null) constructor(accessor: Accessor) : this(persistentHashSetOf(accessor), accessor.hashCode()) - override fun hashCode(): Int = hash + private constructor(set: Set) : this(set, null) + internal constructor(set: PersistentAccessorSet) : this(set, set.hashCode()) + + override fun hashCode(): Int { + cachedHash?.let { return it } + + return set.hashCode().also { cachedHash = it } + } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Concrete) return false - if (hash != other.hash) return false + val currentHash = cachedHash + val otherHash = other.cachedHash + if (currentHash != null && otherHash != null && currentHash != otherHash) return false return set == other.set } override fun contains(accessor: Accessor): Boolean = set.contains(accessor) override fun add(accessor: Accessor): ExclusionSet { - val setWithAccessor = set.add(accessor) + val setWithAccessor = set.persistentAdd(accessor) if (setWithAccessor === set) return this - return Concrete(setWithAccessor, hash + accessor.hashCode()) + return Concrete(setWithAccessor, hashCode() + accessor.hashCode()) } override fun union(other: ExclusionSet): ExclusionSet = when (other) { Empty -> this Universe -> other is Concrete -> { - val union = set.addAll(other.set) - if (union === set) this else Concrete(union, union.hashCode()) + val union = set.persistentAddAll(other.set) + if (union === set) this else Concrete(union) } } @@ -72,21 +83,30 @@ sealed interface ExclusionSet { Empty -> other Universe -> this is Concrete -> { - val intersection = set.retainAll(other.set) + val intersection = set.persistentRetainAll(other.set) when { intersection === set -> this intersection.isEmpty() -> Empty - else -> Concrete(intersection, intersection.hashCode()) + else -> Concrete(intersection) } } } override fun subtract(accessor: Accessor): ExclusionSet { - val subtractResult = set.remove(accessor) + val subtractResult = set.persistentRemove(accessor) return when { subtractResult === set -> this subtractResult.isEmpty() -> Empty - else -> Concrete(subtractResult, hash - accessor.hashCode()) + else -> Concrete(subtractResult, hashCode() - accessor.hashCode()) + } + } + + internal fun subtract(other: Concrete): ExclusionSet { + val subtractResult = set.persistentRemoveAll(other.set) + return when { + subtractResult === set -> this + subtractResult.isEmpty() -> Empty + else -> Concrete(subtractResult) } } @@ -99,3 +119,47 @@ sealed interface ExclusionSet { override fun toString(): String = set.joinToString(prefix = "{", postfix = "}") { it.toSuffix() } } } + +/** Immutable set operations used by compact AP-specific exclusion representations. */ +internal interface PersistentAccessorSet : Set { + fun addPersistent(accessor: Accessor): PersistentAccessorSet + fun addAllPersistent(accessors: Set): PersistentAccessorSet + fun retainAllPersistent(accessors: Set): PersistentAccessorSet + fun removePersistent(accessor: Accessor): PersistentAccessorSet + fun removeAllPersistent(accessors: Set): PersistentAccessorSet +} + +private fun Set.persistentAdd(accessor: Accessor): Set = + when (this) { + is PersistentAccessorSet -> addPersistent(accessor) + is PersistentSet -> add(accessor) + else -> persistentHashSetOf().addAll(this).add(accessor) + } + +private fun Set.persistentAddAll(other: Set): Set = + when (this) { + is PersistentAccessorSet -> addAllPersistent(other) + is PersistentSet -> addAll(other) + else -> persistentHashSetOf().addAll(this).addAll(other) + } + +private fun Set.persistentRetainAll(other: Set): Set = + when (this) { + is PersistentAccessorSet -> retainAllPersistent(other) + is PersistentSet -> retainAll(other) + else -> persistentHashSetOf().addAll(this).retainAll(other) + } + +private fun Set.persistentRemove(accessor: Accessor): Set = + when (this) { + is PersistentAccessorSet -> removePersistent(accessor) + is PersistentSet -> remove(accessor) + else -> persistentHashSetOf().addAll(this).remove(accessor) + } + +private fun Set.persistentRemoveAll(other: Set): Set = + when (this) { + is PersistentAccessorSet -> removeAllPersistent(other) + is PersistentSet -> removeAll(other) + else -> persistentHashSetOf().addAll(this).removeAll(other) + } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt index efe479c8c..935b1c9ab 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt @@ -14,12 +14,17 @@ import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryE import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlySideEffectRequirementDeltaTracker import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.FactToFactTransfer as FactToFactCallTransfer import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.ZeroCallFact import org.opentaint.dataflow.ap.ifds.analysis.MethodCallSummaryHandler import org.opentaint.dataflow.ap.ifds.analysis.MethodCallSummaryHandler.SummaryEdge +import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction.Sequent +import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction.FactToFactTransfer import org.opentaint.dataflow.ap.ifds.analysis.MethodStartFlowFunction.StartFact import org.opentaint.dataflow.ap.ifds.trace.MethodForwardTraceResolver import org.opentaint.dataflow.ap.ifds.trace.MethodForwardTraceResolver.RelevantFactFilter @@ -28,6 +33,9 @@ import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver import org.opentaint.dataflow.ap.ifds.trace.TraceResolverStats import org.opentaint.dataflow.util.Cancellation import org.opentaint.dataflow.util.cartesianProductMapTo +import java.lang.ref.Reference +import java.util.IdentityHashMap +import org.opentaint.ir.api.common.CommonMethod import org.opentaint.ir.api.common.cfg.CommonAssignInst import org.opentaint.ir.api.common.cfg.CommonCallExpr import org.opentaint.ir.api.common.cfg.CommonInst @@ -47,6 +55,8 @@ interface MethodAnalyzer { val containsUnprocessedEdges: Boolean + val containsUnprocessedZeroToZeroEdges: Boolean + val containsDelayedEdges: Boolean fun tabulationAlgorithmStep() @@ -119,7 +129,9 @@ interface MethodAnalyzer { handler: MethodCallResolutionFailureHandler ) - fun methodTraceResolver(): MethodTraceResolver + fun methodTraceResolver( + traceResolutionActionHardLimit: Int? = null, + ): MethodTraceResolver fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -166,6 +178,7 @@ class NormalMethodAnalyzer( private val analysisManager get() = runner.analysisManager private val methodCallResolver get() = runner.methodCallResolver private val cancellation: Cancellation = runner.manager.cancellation + private val softRefManager = runner.manager.refManager.softRefManager(BASE_ONLY_REF_MANAGER) private var zeroInitialFactProcessed: Boolean = false private var initialFacts = apManager.initialFactAbstraction(methodEntryPoint.statement) @@ -173,6 +186,7 @@ class NormalMethodAnalyzer( private var pendingSummaryEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) private var pendingSideEffectRequirements = arrayListOf() private var pendingSideEffectSummaries = arrayListOf() + private var appliedBaseOnlySideEffectRequirements = BaseOnlySideEffectRequirementDeltaTracker() private val analysisContext: MethodAnalysisContext = analysisManager.getMethodAnalysisContext( methodEntryPoint, runner.graph, runner.methodCallResolver, @@ -180,13 +194,32 @@ class NormalMethodAnalyzer( ) private val methodInstGraph = analysisManager.getMethodInstGraph(runner.graph, analysisContext, methodEntryPoint.method) - private var analyzerEnqueued = false - private var unprocessedEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) + private var unprocessedEdges = EdgeCollection.UnprocessedEdgeList(apManager, methodEntryPoint) private var enqueuedUnchangedEdges = EdgeCollection.EdgeSet() + private var pendingBaseOnlyF2F = hashMapOf() + private var pendingBaseOnlyF2FOrder = ArrayDeque() + private var enqueuedUnchangedBaseOnlyF2F = hashMapOf() + private var baseOnlyF2FTransfers: Reference>>? = null + private var unsupportedBaseOnlyF2FTransfers: Reference>? = null + + private fun baseOnlyF2FTransfers(): HashMap> { + baseOnlyF2FTransfers?.get()?.let { return it } + return hashMapOf>() + .also { baseOnlyF2FTransfers = softRefManager.createRef(it) } + } + + private fun unsupportedBaseOnlyF2FTransfers(): HashSet { + unsupportedBaseOnlyF2FTransfers?.get()?.let { return it } + return hashSetOf() + .also { unsupportedBaseOnlyF2FTransfers = softRefManager.createRef(it) } + } override val containsUnprocessedEdges: Boolean - get() = !unprocessedEdges.isEmpty + get() = !unprocessedEdges.isEmpty || pendingBaseOnlyF2F.isNotEmpty() + + override val containsUnprocessedZeroToZeroEdges: Boolean + get() = unprocessedEdges.containsZeroToZeroEdges override var analyzerSteps: Long = 0 private set @@ -194,7 +227,37 @@ class NormalMethodAnalyzer( private val stepsForTaintMark: MutableMap? = taintRulesStatsSamplingPeriod?.let { hashMapOf() } private var summaryEdgesHandled: Long = 0 + private var emittedBaseOnlyNDSummaryResults = hashSetOf() + private var baseOnlyNDSearchCacheVersion = -1L + private var baseOnlyNDSearchCache = hashMapOf>>() + private var baseOnlyMethodCallSummaryHandlers = hashMapOf() + private var baseOnlyPreparedF2FSummaries: Reference>>>? = null + + private fun baseOnlyPreparedF2FSummaries(): HashMap>> { + baseOnlyPreparedF2FSummaries?.get()?.let { return it } + return hashMapOf>>() + .also { baseOnlyPreparedF2FSummaries = softRefManager.createRef(it) } + } + + private var baseOnlyPreparedNDSummaries: Reference>>>? = null + + private fun baseOnlyPreparedNDSummaries(): HashMap>> { + baseOnlyPreparedNDSummaries?.get()?.let { return it } + return hashMapOf>>() + .also { baseOnlyPreparedNDSummaries = softRefManager.createRef(it) } + } + + private val registeredResolvedCallees = hashSetOf() private val traceResolverStats = TraceResolverStats() + @Volatile + private var traceResolverCache: MethodTraceResolver.Cache? = null + + private fun traceResolverCache(): MethodTraceResolver.Cache { + traceResolverCache?.let { return it } + return synchronized(this) { + traceResolverCache ?: MethodTraceResolver.Cache().also { traceResolverCache = it } + } + } private var factDepthLimit = INITIAL_ALLOWED_FACT_DEPTH private var delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) @@ -237,6 +300,7 @@ class NormalMethodAnalyzer( } } } + } override fun allIntraProceduralFacts(): Map> = @@ -271,10 +335,41 @@ class NormalMethodAnalyzer( } override fun tabulationAlgorithmStep() { - analyzerSteps++ + val factToFactGroup = if (apManager is BaseOnlyApManager && unprocessedEdges.isEmpty) { + takeNextBaseOnlyF2FGroup() + } else { + null + } - val edge = unprocessedEdges.removeLast() + if (factToFactGroup != null) { + processFactToFactGroup(factToFactGroup) + } else { + processEdge(unprocessedEdges.removeLast()) + } + + if (containsUnprocessedEdges) return + analyzerEnqueued = false + + // Create new empty list to shrink internal array + unprocessedEdges = EdgeCollection.UnprocessedEdgeList(apManager, methodEntryPoint) + enqueuedUnchangedEdges = EdgeCollection.EdgeSet() + enqueuedUnchangedBaseOnlyF2F = hashMapOf() + + flushPendingSummaryEdges() + flushPendingSideEffectRequirements() + flushPendingSideEffectSummaries() + } + + private fun takeNextBaseOnlyF2FGroup(): FactToFactGroup? { + if (pendingBaseOnlyF2FOrder.isEmpty()) return null + val conclusion = pendingBaseOnlyF2FOrder.removeLast() + val support = checkNotNull(pendingBaseOnlyF2F.remove(conclusion)) + return FactToFactGroup(conclusion, support) + } + + private fun processEdge(edge: Edge, countStep: Boolean = true) { + if (countStep) analyzerSteps++ val finalEdgeFact = when (edge) { is ZeroToZero -> null is ZeroToFact -> edge.factAp @@ -298,18 +393,70 @@ class NormalMethodAnalyzer( simpleStatementStep(edge) } } + } - if (!unprocessedEdges.isEmpty) return + private fun processFactToFactGroup(group: FactToFactGroup) { + val conclusion = group.conclusion + val statement = conclusion.statement + val finalFact = conclusion.finalFact + val initialFacts = group.initialFacts - analyzerEnqueued = false + if (methodInstGraph.isExitPoint(analysisManager, statement)) { + group.forEachEdge(methodEntryPoint, ::processEdge) + return + } - // Create new empty list to shrink internal array - unprocessedEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) - enqueuedUnchangedEdges = EdgeCollection.EdgeSet() + if (!analysisManager.isReachable(apManager, analysisContext, finalFact.base, statement)) { + analyzerSteps += initialFacts.size + return + } + analysisManager.onInstructionReached(statement) + + val callExpr = analysisManager.getCallExpr(statement) + if (callExpr != null) { + val returnValue: CommonValue? = (statement as? CommonAssignInst)?.lhv + val flowFunction = analysisManager.getMethodCallFlowFunction( + apManager, + analysisContext, + returnValue, + callExpr, + statement, + generateTrace = false, + ) + val transfer = flowFunction.createFactToFactTransfer(finalFact) + if (transfer != null) { + analyzerSteps++ + transfer.forEach { output -> + when (output) { + FactToFactCallTransfer.Unchanged -> propagateUnchangedFactGroup(group) + } + } + return + } + analyzerSteps += initialFacts.size + group.forEachEdge(methodEntryPoint) { callStatementStep(callExpr, it, flowFunction) } + return + } - flushPendingSummaryEdges() - flushPendingSideEffectRequirements() - flushPendingSideEffectSummaries() + val flowFunction = analysisManager.getMethodSequentFlowFunction( + apManager, + analysisContext, + statement, + ) + val transfer = baseOnlyFactToFactTransfer(flowFunction, conclusion) + if (transfer == null) { + analyzerSteps += initialFacts.size + group.forEachEdge(methodEntryPoint) { supportedEdge -> + handleSequentFact( + supportedEdge, + flowFunction.propagateFactToFact(supportedEdge.initialFactAp, supportedEdge.factAp), + ) + } + return + } + + analyzerSteps++ + applyBaseOnlyFactToFactTransfer(group, transfer) } private fun simpleStatementStep(edge: Edge) { @@ -325,6 +472,57 @@ class NormalMethodAnalyzer( handleSequentFact(edge, sequentialFacts) } + private fun baseOnlyFactToFactTransfer( + flowFunction: MethodSequentFlowFunction, + conclusion: F2FConclusion, + ): Set? { + val transfers = baseOnlyF2FTransfers() + transfers[conclusion]?.let { return it } + val unsupportedTransfers = unsupportedBaseOnlyF2FTransfers() + if (conclusion in unsupportedTransfers) return null + + val transfer = flowFunction.createFactToFactTransfer(conclusion.finalFact) + if (transfer == null) { + unsupportedTransfers += conclusion + return null + } + transfers[conclusion] = transfer + return transfer + } + + private fun applyBaseOnlyFactToFactTransfer( + group: FactToFactGroup, + transfer: Set, + ) { + check(!methodInstGraph.isExitPoint(analysisManager, group.conclusion.statement)) { + "Grouped fact-to-fact transfer is not valid at a method exit" + } + + transfer.forEach { output -> + when (output) { + FactToFactTransfer.Unchanged -> propagateUnchangedFactGroup(group) + is FactToFactTransfer.Fact -> propagateChangedFactGroup( + group.initialFacts, + group.conclusion.statement, + output.factAp, + ) + is FactToFactTransfer.ExcludeAccessor -> { + val refinedInitials = InitialFactSupport() + group.initialFacts.forEach { initial -> + val refined = initial.replaceExclusions(output.excludedFactAp.exclusions) + handleInputFactChange(initial, refined) + refinedInitials.add(refined) + } + propagateChangedFactGroup( + refinedInitials, + group.conclusion.statement, + output.excludedFactAp, + ) + } + } + } + } + private fun handleSequentFact(edge: Edge, sf: Iterable) = sf.forEach { handleSequentFact(edge, it) } @@ -356,10 +554,14 @@ class NormalMethodAnalyzer( handleStatementEdge(edge, edgeAfterStatement) } - private fun callStatementStep(callExpr: CommonCallExpr, edge: Edge) { + private fun callStatementStep( + callExpr: CommonCallExpr, + edge: Edge, + preparedFlowFunction: MethodCallFlowFunction? = null, + ) { val returnValue: CommonValue? = (edge.statement as? CommonAssignInst)?.lhv - val flowFunction = analysisManager.getMethodCallFlowFunction( + val flowFunction = preparedFlowFunction ?: analysisManager.getMethodCallFlowFunction( apManager, analysisContext, returnValue, @@ -596,20 +798,81 @@ class NormalMethodAnalyzer( } private fun addSequentialUnchangedEdge(edge: Edge) { - if (enqueuedUnchangedEdges.add(edge)) { - enqueueNewEdge(edge) + enqueueUnchangedBoundary(edge) + } + + private fun enqueueUnchangedBoundary(edge: Edge) { + if (enqueuedUnchangedEdges.add(edge)) enqueueNewEdge(edge) + } + + private fun enqueueUnchangedBoundary(group: FactToFactGroup) { + val seen = enqueuedUnchangedBaseOnlyF2F.getOrPut(group.conclusion, ::InitialFactSupport) + val added = InitialFactSupport() + group.initialFacts.forEach { initial -> + if (seen.add(initial)) added.add(initial) } + if (!added.isEmpty) enqueueBaseOnlyF2F(group.conclusion, added) } - private fun enqueueNewEdge(edge: Edge) { - unprocessedEdges.add(edge) + private fun propagateUnchangedFactGroup(group: FactToFactGroup) { + methodInstGraph.forEachSuccessor(analysisManager, group.conclusion.statement) { successor -> + enqueueUnchangedBoundary(group.withStatement(successor)) + } + } + private fun propagateChangedFactGroup( + initialFacts: InitialFactSupport, + statement: CommonInst, + finalFact: FinalFactAp, + ) { + methodInstGraph.forEachSuccessor(analysisManager, statement) { successor -> + edges.addFactToFactSupports(successor, initialFacts, finalFact) { initial, addedFinal -> + enqueueBaseOnlyF2F(F2FConclusion(successor, addedFinal), initial) + } + } + } + + private fun enqueueBaseOnlyF2F(conclusion: F2FConclusion, initial: InitialFactAp) { + val support = pendingBaseOnlyF2F.getOrPut(conclusion) { + pendingBaseOnlyF2FOrder.addLast(conclusion) + InitialFactSupport() + } + if (support.add(initial)) enqueueAnalyzer() + } + + private fun enqueueBaseOnlyF2F(conclusion: F2FConclusion, initials: InitialFactSupport) { + val support = pendingBaseOnlyF2F.getOrPut(conclusion) { + pendingBaseOnlyF2FOrder.addLast(conclusion) + InitialFactSupport() + } + val changed = support.addAll(initials) + if (changed) enqueueAnalyzer() + } + + private fun enqueueAnalyzer() { if (!analyzerEnqueued) { runner.enqueueMethodAnalyzer(this) analyzerEnqueued = true } } + private fun enqueueNewEdge(edge: Edge) { + if (apManager is BaseOnlyApManager && edge is FactToFact) { + val conclusion = F2FConclusion(edge.statement, edge.factAp) + enqueueBaseOnlyF2F(conclusion, edge.initialFactAp) + } else { + val zeroToZeroPriorityChanged = + edge is ZeroToZero && !unprocessedEdges.containsZeroToZeroEdges + unprocessedEdges.add(edge) + + if (analyzerEnqueued && zeroToZeroPriorityChanged) { + runner.reprioritizeMethodAnalyzer(this) + } + } + + enqueueAnalyzer() + } + private fun handleInputFactChange(originalInputFactAp: InitialFactAp, newInputFactAp: InitialFactAp) { if (originalInputFactAp == newInputFactAp) return initialFacts.registerNewInitialFact(newInputFactAp, analysisManager.factTypeChecker).forEach { (initialFact, finalFact) -> @@ -738,13 +1001,46 @@ class NormalMethodAnalyzer( } override fun handleResolvedMethodCall(method: MethodWithContext, handler: MethodCallHandler) { - for (ep in methodEntryPoints(method)) { + registerResolvedMethodCall(method.method) + val analysisMethod = analysisMethod(method, handler) + for (ep in methodEntryPoints(analysisMethod)) { handleMethodCall(handler, ep) } } override fun handleResolvedMethodCall(entryPoint: MethodEntryPoint, handler: MethodCallHandler) { - handleMethodCall(handler, entryPoint) + registerResolvedMethodCall(entryPoint.method) + val analysisMethod = analysisMethod(MethodWithContext(entryPoint.method, entryPoint.context), handler) + val analysisEntryPoint = MethodEntryPoint(analysisMethod.ctx, entryPoint.statement) + handleMethodCall(handler, analysisEntryPoint) + } + + private fun analysisMethod(method: MethodWithContext, handler: MethodCallHandler): MethodWithContext { + val manager = analysisManager as? TaintAnalysisManager ?: return method + val contextIndependentFact = handler is MethodCallHandler.ZeroToZeroHandler || + handler.currentEdge().finalFactBase == AccessPathBase.ClassStatic + return manager.overApproximateMethodContext(method, contextIndependentFact) + } + + private val Edge.finalFactBase: AccessPathBase? + get() = when (this) { + is ZeroToZero -> null + is ZeroToFact -> factAp.base + is FactToFact -> factAp.base + is NDFactToFact -> factAp.base + } + + private fun registerResolvedMethodCall(callee: CommonMethod) { + if (registeredResolvedCallees.add(callee)) { + runner.manager.registerResolvedMethodCall(methodEntryPoint.method, callee) + } + } + + private fun MethodCallHandler.currentEdge(): Edge = when (this) { + is MethodCallHandler.ZeroToZeroHandler -> currentEdge + is MethodCallHandler.ZeroToFactHandler -> currentEdge + is MethodCallHandler.FactToFactHandler -> currentEdge + is MethodCallHandler.NDFactToFactHandler -> currentEdge } private fun handleMethodCall(handler: MethodCallHandler, ep: MethodEntryPoint) = when (handler) { @@ -917,9 +1213,15 @@ class NormalMethodAnalyzer( } private fun addSideEffectRequirement(curInitialFactAp: InitialFactAp, sideEffectRequirement: InitialFactAp) { - handleInputFactChange(curInitialFactAp, sideEffectRequirement) + val requirementDelta = if (apManager is BaseOnlyApManager) { + appliedBaseOnlySideEffectRequirements.add(curInitialFactAp, sideEffectRequirement) ?: return + } else { + sideEffectRequirement + } + + handleInputFactChange(curInitialFactAp, requirementDelta) - pendingSideEffectRequirements.add(sideEffectRequirement) + pendingSideEffectRequirements.add(requirementDelta) if (!analyzerEnqueued) { flushPendingSideEffectRequirements() @@ -955,11 +1257,8 @@ class NormalMethodAnalyzer( methodSummaries: List ) { summaryEdgesHandled++ - val applicableSummaries = methodSummaries.filter { isApplicableExitToReturnEdge(it) } - val handler = analysisManager.getMethodCallSummaryHandler( - apManager, analysisContext, currentEdge.statement - ) + val handler = methodCallSummaryHandler(currentEdge.statement) for (methodSummary in applicableSummaries) { if (!cancellation.isActive()) return @@ -977,17 +1276,16 @@ class NormalMethodAnalyzer( methodSummaries: List ) { summaryEdgesHandled++ - val applicableSummaries = methodSummaries.filter { isApplicableExitToReturnEdge(it) } for (sub in summarySubs) { if (!cancellation.isActive()) return - val handler = analysisManager.getMethodCallSummaryHandler( - apManager, analysisContext, sub.currentEdge.statement - ) + val handler = methodCallSummaryHandler(sub.currentEdge.statement) - val summariesToApply = applicableSummaries.flatMap { handler.prepareFactToFactSummary(it) } + val summariesToApply = applicableSummaries.flatMap { + prepareFactToFactSummary(sub.currentEdge.statement, handler, it) + } applyMethodSummaries( currentEdge = sub.currentEdge, @@ -1014,17 +1312,16 @@ class NormalMethodAnalyzer( methodSummaries: List ) { summaryEdgesHandled++ - val applicableSummaries = methodSummaries.filter { isApplicableExitToReturnEdge(it) } for (sub in summarySubs) { if (!cancellation.isActive()) return - val handler = analysisManager.getMethodCallSummaryHandler( - apManager, analysisContext, sub.currentEdge.statement - ) + val handler = methodCallSummaryHandler(sub.currentEdge.statement) - val summariesToApply = applicableSummaries.flatMap { handler.prepareFactToFactSummary(it) } + val summariesToApply = applicableSummaries.flatMap { + prepareFactToFactSummary(sub.currentEdge.statement, handler, it) + } applyMethodSummaries( currentEdge = sub.currentEdge, @@ -1053,17 +1350,16 @@ class NormalMethodAnalyzer( methodSummaries: List, ) { summaryEdgesHandled++ - val applicableSummaries = methodSummaries.filter { isApplicableExitToReturnEdge(it) } for (sub in summarySubs) { if (!cancellation.isActive()) return - val handler = analysisManager.getMethodCallSummaryHandler( - apManager, analysisContext, sub.currentEdge.statement - ) + val handler = methodCallSummaryHandler(sub.currentEdge.statement) - val summariesToApply = applicableSummaries.flatMap { handler.prepareFactToFactSummary(it) } + val summariesToApply = applicableSummaries.flatMap { + prepareFactToFactSummary(sub.currentEdge.statement, handler, it) + } applyMethodSummaries( currentEdge = sub.currentEdge, @@ -1132,6 +1428,8 @@ class NormalMethodAnalyzer( handleSummary: (currentFactAp: FinalFactAp, summaryEffect: SummaryEdgeApplication, S) -> Set ) { val methodInitialFact = currentEdgeFactAp.rebase(methodInitialFactBase) + val resultingSequents: MutableSet? = + if (apManager is BaseOnlyApManager) hashSetOf() else null val summaries = methodSummaries.groupByTo(hashMapOf()) { getSummaryInitialFact(it) } for ((summaryInitialFact, summaryEdges) in summaries) { @@ -1140,16 +1438,21 @@ class NormalMethodAnalyzer( val summaryEdgeEffects = MethodSummaryEdgeApplicationUtils.tryApplySummaryEdge( methodInitialFact, summaryInitialFact ) - for (summaryEdgeEffect in summaryEdgeEffects) { for (methodSummary in summaryEdges) { if (!cancellation.isActive()) return - val sf = handleSummary(currentEdgeFactAp, summaryEdgeEffect, methodSummary) - handleSequentFact(currentEdge, sf) + val sequents = handleSummary(currentEdgeFactAp, summaryEdgeEffect, methodSummary) + if (resultingSequents != null) { + resultingSequents += sequents + } else { + handleSequentFact(currentEdge, sequents) + } } } } + + resultingSequents?.let { handleSequentFact(currentEdge, it) } } private inline fun handleMethodNDSummariesSub( @@ -1168,11 +1471,11 @@ class NormalMethodAnalyzer( val currentEdge = sub.subEdge() - val handler = analysisManager.getMethodCallSummaryHandler( - apManager, analysisContext, currentEdge.statement - ) + val handler = methodCallSummaryHandler(currentEdge.statement) - val summariesToApply = applicableSummaries.flatMap { handler.prepareNDFactToFactSummary(it) } + val summariesToApply = applicableSummaries.flatMap { + prepareNDFactToFactSummary(currentEdge.statement, handler, it) + } applyMethodNDSummaries( summaryHandler = handler, @@ -1195,6 +1498,8 @@ class NormalMethodAnalyzer( nextSummary@for (summaryEdge in methodSummaries) { if (!cancellation.isActive()) return + val deduplicateConjunctiveResult = + apManager is BaseOnlyApManager && summaryEdge.initialFacts.size > 1 val requiredFacts = mutableListOf() for (summaryInitialFact in summaryEdge.initialFacts) { @@ -1207,27 +1512,13 @@ class NormalMethodAnalyzer( val requiredInitials = mutableListOf>>() for (requiredFact in requiredFacts) { - - val searcher = object : MethodAnalyzerEdgeSearcher( - edges, apManager, analysisManager, analysisContext, methodInstGraph - ) { - override fun matchFact(factAtStatement: FinalFactAp, targetFactPattern: InitialFactAp): Boolean = - factAtStatement.rebase(requiredFact.base).matchNDInitial(requiredFact) - } - - val mappedRequiredFacts = analysisContext.methodCallFactMapper.mapMethodExitToReturnFlowFact( - currentEdge.statement, requiredFact - ) - - val factInitials = mappedRequiredFacts.flatMapTo(hashSetOf()) { - searcher.findMatchingEdgesInitialFacts(currentEdge.statement, it) - } + val factInitials = findNDRequiredInitials(currentEdge.statement, requiredFact) if (factInitials.isEmpty()) { continue@nextSummary } - requiredInitials.add(factInitials.toList()) + requiredInitials.add(factInitials) } requiredInitials.cartesianProductMapTo { initialFactGroup -> @@ -1302,8 +1593,87 @@ class NormalMethodAnalyzer( } val applicableSf = sf.filter { it !is Sequent.SideEffectRequirement } - handleSequentFact(currentEdge, applicableSf) + if (!deduplicateConjunctiveResult) { + handleSequentFact(currentEdge, applicableSf) + return@cartesianProductMapTo + } + + for (sequent in applicableSf) { + val result = BaseOnlyNDSummaryResult(currentEdge.statement, summaryEdge, sequent) + if (emittedBaseOnlyNDSummaryResults.add(result)) { + handleSequentFact(currentEdge, sequent) + } + } + } + } + } + + private fun findNDRequiredInitials( + callStatement: CommonInst, + requiredFact: InitialFactAp, + ): List> { + fun compute(): List> { + val searcher = object : MethodAnalyzerEdgeSearcher( + edges, apManager, analysisManager, analysisContext, methodInstGraph + ) { + override fun matchFact( + factAtStatement: FinalFactAp, + targetFactPattern: InitialFactAp, + ): Boolean = factAtStatement.rebase(requiredFact.base).matchNDInitial(requiredFact) } + return analysisContext.methodCallFactMapper.mapMethodExitToReturnFlowFact( + callStatement, requiredFact + ).flatMapTo(hashSetOf()) { + searcher.findMatchingEdgesInitialFacts(callStatement, it) + }.toList() + } + + if (apManager !is BaseOnlyApManager) return compute() + + val edgeVersion = edges.modificationVersion + if (baseOnlyNDSearchCacheVersion != edgeVersion) { + baseOnlyNDSearchCacheVersion = edgeVersion + baseOnlyNDSearchCache.clear() + } + + val key = NDSearchKey(callStatement, requiredFact) + baseOnlyNDSearchCache[key]?.let { return it } + + val result = compute() + baseOnlyNDSearchCache[key] = result + return result + } + + private fun methodCallSummaryHandler(statement: CommonInst): MethodCallSummaryHandler { + if (apManager !is BaseOnlyApManager) { + return analysisManager.getMethodCallSummaryHandler(apManager, analysisContext, statement) + } + return baseOnlyMethodCallSummaryHandlers.getOrPut(statement) { + analysisManager.getMethodCallSummaryHandler(apManager, analysisContext, statement) + } + } + + private fun prepareFactToFactSummary( + statement: CommonInst, + handler: MethodCallSummaryHandler, + summary: FactToFact, + ): List { + if (apManager !is BaseOnlyApManager) return handler.prepareFactToFactSummary(summary) + val summariesAtStatement = baseOnlyPreparedF2FSummaries().getOrPut(statement) { IdentityHashMap() } + return summariesAtStatement.getOrPut(summary) { + handler.prepareFactToFactSummary(summary) + } + } + + private fun prepareNDFactToFactSummary( + statement: CommonInst, + handler: MethodCallSummaryHandler, + summary: NDFactToFact, + ): List { + if (apManager !is BaseOnlyApManager) return handler.prepareNDFactToFactSummary(summary) + val summariesAtStatement = baseOnlyPreparedNDSummaries().getOrPut(statement) { IdentityHashMap() } + return summariesAtStatement.getOrPut(summary) { + handler.prepareNDFactToFactSummary(summary) } } @@ -1318,8 +1688,17 @@ class NormalMethodAnalyzer( return true } - override fun methodTraceResolver(): MethodTraceResolver = - MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph) + override fun methodTraceResolver( + traceResolutionActionHardLimit: Int?, + ): MethodTraceResolver = MethodTraceResolver( + runner, + traceResolverStats, + analysisContext, + edges, + methodInstGraph, + traceResolutionActionHardLimit, + traceResolverCache(), + ) override fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -1369,12 +1748,30 @@ class NormalMethodAnalyzer( } private fun resetEdgeProcessingStorage(apManager: ApManager) { - unprocessedEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) + analyzerEnqueued = false + traceResolverCache = null + unprocessedEdges = EdgeCollection.UnprocessedEdgeList(apManager, methodEntryPoint) enqueuedUnchangedEdges = EdgeCollection.EdgeSet() + enqueuedUnchangedBaseOnlyF2F.clear() + pendingBaseOnlyF2F.clear() + pendingBaseOnlyF2FOrder.clear() + baseOnlyF2FTransfers?.clear() + baseOnlyF2FTransfers = null + unsupportedBaseOnlyF2FTransfers?.clear() + unsupportedBaseOnlyF2FTransfers = null pendingSummaryEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) pendingSideEffectRequirements = arrayListOf() pendingSideEffectSummaries = arrayListOf() + appliedBaseOnlySideEffectRequirements = BaseOnlySideEffectRequirementDeltaTracker() + emittedBaseOnlyNDSummaryResults = hashSetOf() + baseOnlyNDSearchCacheVersion = -1L + baseOnlyNDSearchCache = hashMapOf() + baseOnlyMethodCallSummaryHandlers = hashMapOf() + baseOnlyPreparedF2FSummaries?.clear() + baseOnlyPreparedF2FSummaries = null + baseOnlyPreparedNDSummaries?.clear() + baseOnlyPreparedNDSummaries = null delayedF2FSummaries = EdgeCollection.EdgeList(apManager, methodEntryPoint) initialFacts = apManager.initialFactAbstraction(methodEntryPoint.statement) @@ -1384,7 +1781,85 @@ class NormalMethodAnalyzer( companion object { const val INITIAL_ALLOWED_FACT_DEPTH = 3 const val DEBUG_ANALYSIS_TIME = false + const val BASE_ONLY_REF_MANAGER = "BaseOnly" + } + + private data class BaseOnlyNDSummaryResult( + val statement: CommonInst, + val preparedSummary: NDFactToFact, + val sequent: Sequent, + ) + + private data class NDSearchKey( + val statement: CommonInst, + val requiredFact: InitialFactAp, + ) + + private data class F2FConclusion( + val statement: CommonInst, + val finalFact: FinalFactAp, + ) + + private class InitialFactSupport : Iterable { + private var first: InitialFactAp? = null + private var multiple: MutableSet? = null + + val size: Int get() = multiple?.size ?: if (first == null) 0 else 1 + val isEmpty: Boolean get() = first == null + + fun add(fact: InitialFactAp): Boolean { + val facts = multiple + if (facts != null) { + return facts.add(fact) + } + + val current = first + if (current == null) { + first = fact + return true + } else if (current != fact) { + multiple = linkedSetOf(current, fact) + return true + } + return false + } + + fun addAll(other: InitialFactSupport): Boolean { + var changed = false + other.forEach { changed = add(it) || changed } + return changed + } + + fun first(): InitialFactAp = first ?: error("Empty initial fact support") + + inline fun forEach(action: (InitialFactAp) -> Unit) { + multiple?.forEach(action) ?: action(first()) + } + + override fun iterator(): Iterator = + multiple?.iterator() ?: listOf(first()).iterator() + } + + private data class FactToFactGroup( + val conclusion: F2FConclusion, + val initialFacts: InitialFactSupport, + ) { + fun firstEdge(methodEntryPoint: MethodEntryPoint): FactToFact = + FactToFact(methodEntryPoint, initialFacts.first(), conclusion.statement, conclusion.finalFact) + + inline fun forEachEdge( + methodEntryPoint: MethodEntryPoint, + action: (FactToFact) -> Unit, + ) { + initialFacts.forEach { initial -> + action(FactToFact(methodEntryPoint, initial, conclusion.statement, conclusion.finalFact)) + } + } + + fun withStatement(statement: CommonInst): FactToFactGroup = + copy(conclusion = F2FConclusion(statement, conclusion.finalFact)) } + } class EmptyMethodAnalyzer( @@ -1442,6 +1917,9 @@ class EmptyMethodAnalyzer( override val containsUnprocessedEdges: Boolean get() = false + override val containsUnprocessedZeroToZeroEdges: Boolean + get() = false + override val containsDelayedEdges: Boolean get() = false @@ -1550,7 +2028,9 @@ class EmptyMethodAnalyzer( error("Empty method should not method resolution results") } - override fun methodTraceResolver(): MethodTraceResolver { + override fun methodTraceResolver( + traceResolutionActionHardLimit: Int?, + ): MethodTraceResolver { error("Empty method has no trace") } @@ -1635,6 +2115,9 @@ class TimedMethodAnalyzer( override val containsUnprocessedEdges: Boolean get() = base.containsUnprocessedEdges + override val containsUnprocessedZeroToZeroEdges: Boolean + get() = base.containsUnprocessedZeroToZeroEdges + override val containsDelayedEdges: Boolean get() = base.containsDelayedEdges @@ -1818,7 +2301,9 @@ class TimedMethodAnalyzer( base.handleMethodCallResolutionFailure(callExpr, handler) } - override fun methodTraceResolver(): MethodTraceResolver = base.methodTraceResolver() + override fun methodTraceResolver( + traceResolutionActionHardLimit: Int?, + ): MethodTraceResolver = base.methodTraceResolver(traceResolutionActionHardLimit) override fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -1882,4 +2367,4 @@ private class TaintMarkGatherer: FactTypeChecker.FactApFilter { else -> FactTypeChecker.FilterResult.FilterNext(this) } } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt index 999c6bac3..1c540f96d 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt @@ -2,9 +2,11 @@ package org.opentaint.dataflow.ap.ifds import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.AbstractStaticEdges.Companion.isAbstractStaticEdge import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.MethodEdgesInitialToFinalApSet import org.opentaint.ir.api.common.cfg.CommonInst import java.util.BitSet @@ -13,17 +15,23 @@ class MethodAnalyzerEdges( private val methodEntryPoint: MethodEntryPoint, languageManager: LanguageManager ) { + var modificationVersion: Long = 0 + private set + private val maxInstIdx = languageManager.getMaxInstIndex(methodEntryPoint.method) private val zeroToZeroEdges = SameInitialZeroFactEdges(maxInstIdx, languageManager) private val zeroToFactEdges = apManager.methodEdgesFinalApSet(methodEntryPoint.statement, maxInstIdx, languageManager) + private val abstractStaticEdges = AbstractStaticEdges(apManager, maxInstIdx, languageManager) private val taintedToFactEdges = apManager.methodEdgesInitialToFinalApSet(methodEntryPoint.statement, maxInstIdx, languageManager) private val ndFactToFactEdges = apManager.methodEdgesNDInitialToFinalApSet(methodEntryPoint.statement, maxInstIdx, languageManager) fun add(edge: Edge): List { check(edge.methodEntryPoint == methodEntryPoint) - return addEdge(edge) + return addEdge(edge).also { added -> + if (added.isNotEmpty()) modificationVersion++ + } } fun reachedStatements() = zeroToZeroEdges.reachedStatements() @@ -37,6 +45,7 @@ class MethodAnalyzerEdges( val ndf2f = mutableListOf, FinalFactAp>>() zeroToFactEdges.collectApAtStatement(z2f, stmt) + abstractStaticEdges.collectApAtStatement(f2f, stmt) taintedToFactEdges.collectApAtStatement(f2f, stmt) ndFactToFactEdges.collectApAtStatement(ndf2f, stmt) @@ -96,18 +105,45 @@ class MethodAnalyzerEdges( val initialAp = edge.initialFactAp val finalAp = edge.factAp - val (addedInitial, addedFinal) = taintedToFactEdges.add(edge.statement, initialAp, finalAp) ?: return emptyList() + val storage = if (isAbstractStaticEdge(initialAp, finalAp)) abstractStaticEdges else taintedToFactEdges + return storage.add(edge.statement, initialAp, finalAp).map { (addedInitial, addedFinal) -> + if (addedInitial === initialAp && addedFinal === finalAp) { + edge + } else { + Edge.FactToFact( + methodEntryPoint = edge.methodEntryPoint, + initialFactAp = addedInitial, + statement = edge.statement, + factAp = addedFinal, + ) + } + } + } - if (addedInitial === initialAp && addedFinal === finalAp) return listOf(edge) + fun addFactToFactSupports( + statement: CommonInst, + initialFacts: Iterable, + finalFact: FinalFactAp, + emitDelta: (InitialFactAp, FinalFactAp) -> Unit, + ) { + var changed = false + val onDelta: (InitialFactAp, FinalFactAp) -> Unit = { initial, addedFinal -> + changed = true + emitDelta(initial, addedFinal) + } - return listOf( - Edge.FactToFact( - methodEntryPoint = edge.methodEntryPoint, - initialFactAp = addedInitial, - statement = edge.statement, - factAp = addedFinal - ) - ) + if (finalFact.base is AccessPathBase.ClassStatic && finalFact.depth == 0) { + initialFacts.forEach { initial -> + val storage = if (isAbstractStaticEdge(initial, finalFact)) abstractStaticEdges else taintedToFactEdges + storage.add(statement, initial, finalFact).forEach { (addedInitial, addedFinal) -> + onDelta(addedInitial, addedFinal) + } + } + } else { + taintedToFactEdges.addAll(statement, initialFacts, finalFact, onDelta) + } + + if (changed) modificationVersion++ } fun allZeroToFactFactsAtStatement(statement: CommonInst, finalFactPattern: InitialFactAp): List { @@ -116,8 +152,15 @@ class MethodAnalyzerEdges( return result } + fun allZeroToFactFactsAtStatement(statement: CommonInst): List { + val result = mutableListOf() + zeroToFactEdges.collectApAtStatement(result, statement) + return result + } + fun allFactToFactFactsAtStatement(statement: CommonInst, finalFactPattern: InitialFactAp): List> { val result = mutableListOf>() + abstractStaticEdges.collectApAtStatement(result, statement, finalFactPattern) taintedToFactEdges.collectApAtStatement(result, statement, finalFactPattern) return result } @@ -130,6 +173,7 @@ class MethodAnalyzerEdges( fun allFactToFactFactsAtStatement(statement: CommonInst, initialFactAp: InitialFactAp, finalFactPattern: InitialFactAp): List { val result = mutableListOf() + abstractStaticEdges.collectApAtStatement(result, statement, initialFactAp, finalFactPattern) taintedToFactEdges.collectApAtStatement(result, statement, initialFactAp, finalFactPattern) return result } @@ -140,6 +184,7 @@ class MethodAnalyzerEdges( return result } + private class SameInitialZeroFactEdges( maxInstIdx: Int, private val languageManager: LanguageManager @@ -157,6 +202,81 @@ class MethodAnalyzerEdges( fun reachedStatements(): BitSet = edges } + private class AbstractStaticEdges( + apManager: ApManager, + maxInstIdx: Int, + private val languageManager: LanguageManager + ): MethodEdgesInitialToFinalApSet { + private val initial = apManager.mostAbstractInitialAp(AccessPathBase.ClassStatic) + private val final = apManager.mostAbstractFinalAp(AccessPathBase.ClassStatic) + + private val exclusions = arrayOfNulls(instructionStorageSize(maxInstIdx)) + + override fun add( + statement: CommonInst, + initialAp: InitialFactAp, + finalAp: FinalFactAp + ): List> { + val edgeIdx = instructionStorageIdx(statement, languageManager) + val exclusion = finalAp.exclusions + val currentExclusion = exclusions[edgeIdx] + + if (currentExclusion == null) { + exclusions[edgeIdx] = exclusion + return listOf(initialAp to finalAp) + } + + val mergedExclusion = currentExclusion.union(exclusion) + if (mergedExclusion === currentExclusion) return emptyList() + + exclusions[edgeIdx] = mergedExclusion + return listOf( + initialAp.replaceExclusions(mergedExclusion) to finalAp.replaceExclusions(mergedExclusion) + ) + } + + override fun collectApAtStatement( + collection: MutableList>, + statement: CommonInst + ) { + val exclusion = exclusionAt(statement) ?: return + collection += initial.replaceExclusions(exclusion) to final.replaceExclusions(exclusion) + } + + override fun collectApAtStatement( + collection: MutableList>, + statement: CommonInst, + finalFactPattern: InitialFactAp + ) { + if (finalFactPattern.base != AccessPathBase.ClassStatic) return + collectApAtStatement(collection, statement) + } + + override fun collectApAtStatement( + collection: MutableList, + statement: CommonInst, + initialAp: InitialFactAp, + finalFactPattern: InitialFactAp + ) { + if (initialAp.base != AccessPathBase.ClassStatic || initialAp.depth != 0) return + if (finalFactPattern.base != AccessPathBase.ClassStatic) return + + val exclusion = exclusionAt(statement) ?: return + collection += final.replaceExclusions(exclusion) + } + + private fun exclusionAt(statement: CommonInst): ExclusionSet? = + exclusions[instructionStorageIdx(statement, languageManager)] + + companion object { + fun isAbstractStaticEdge(initialAp: InitialFactAp, finalAp: FinalFactAp): Boolean = + initialAp.base is AccessPathBase.ClassStatic + && initialAp.depth == 0 + && finalAp.base is AccessPathBase.ClassStatic + && finalAp.depth == 0 + } + } + abstract class EdgeStorage(initialStatement: CommonInst) : AccessPathBaseStorage(initialStatement) { private var locals: Int2ObjectOpenHashMap? = null @@ -185,8 +305,6 @@ class MethodAnalyzerEdges( override fun forEachConstantValue(body: (AccessPathBase, Storage) -> Unit) { constants?.forEach { body(it.key, it.value) } } - - } companion object { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt index e54ea56cb..897f31fb5 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt @@ -78,6 +78,14 @@ open class MethodSummariesUnitStorage( return methodStorage.factToFactEdges(finalFactBase) } + fun methodFactToFactSummaryEdges( + methodEntryPoint: MethodEntryPoint, + finalFactPattern: FinalFactAp, + ): List { + val methodStorage = methodSummaryEdges(methodEntryPoint) + return methodStorage.factToFactEdges(finalFactPattern) + } + fun methodFactNDSummaries( methodEntryPoint: MethodEntryPoint, finalFactBase: AccessPathBase diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodTaintMarkReachabilityIndex.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodTaintMarkReachabilityIndex.kt new file mode 100644 index 000000000..78ebd991d --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodTaintMarkReachabilityIndex.kt @@ -0,0 +1,165 @@ +package org.opentaint.dataflow.ap.ifds + +import org.opentaint.dataflow.ap.ifds.access.FactAp +import java.util.concurrent.ConcurrentHashMap + +data class MethodTaintMarkState( + val method: Method, + val mark: String, +) + +data class TaintMarkTransition( + val inputMark: String, + val outputMark: String, +) + +data class MethodTaintMarkSummaryStats( + val methods: Int, + val transitions: Int, +) + +internal class MethodTaintMarkReachabilityIndex { + private class MethodSummary { + val inputMarks = ConcurrentHashMap.newKeySet() + val outputMarks = ConcurrentHashMap.newKeySet() + val transitions = ConcurrentHashMap.newKeySet() + } + + private val callers = ConcurrentHashMap>() + private val callees = ConcurrentHashMap>() + private val summaries = ConcurrentHashMap() + + fun addCall(caller: Method, callee: Method) { + callers.computeIfAbsent(callee) { ConcurrentHashMap.newKeySet() }.add(caller) + callees.computeIfAbsent(caller) { ConcurrentHashMap.newKeySet() }.add(callee) + } + + fun addSummaryEdges(method: Method, edges: List) { + edges.forEach { edge -> + when (edge) { + is Edge.ZeroToZero -> Unit + is Edge.ZeroToFact -> recordOutput(method, edge.factAp.taintMarks()) + is Edge.FactToFact -> recordTransition( + method, + edge.initialFactAp.taintMarks(), + edge.factAp.taintMarks(), + ) + is Edge.NDFactToFact -> edge.initialFacts.forEach { initial -> + recordTransition(method, initial.taintMarks(), edge.factAp.taintMarks()) + } + } + } + } + + fun clearSummaries() = summaries.clear() + + fun methodsThatCanReach(method: Method): Set { + val reachable = hashSetOf(method) + val pending = ArrayDeque() + pending.addLast(method) + + while (pending.isNotEmpty()) { + val callee = pending.removeFirst() + for (caller in callers[callee].orEmpty()) { + if (reachable.add(caller)) pending.addLast(caller) + } + } + + return reachable + } + + fun statesThatCanReach( + targetMethod: Method, + targetMarks: Set, + ruleTransitions: Map>, + relevantMarks: Set, + ): Set> { + if (targetMarks.isEmpty() || relevantMarks.isEmpty()) return emptySet() + + val reverseRuleTransitions = ruleTransitions.mapValues { (_, transitions) -> + transitions.asSequence() + .filter { it.inputMark in relevantMarks && it.outputMark in relevantMarks } + .groupBy({ it.outputMark }, { it.inputMark }) + } + val reachable = hashSetOf>() + val pending = ArrayDeque>() + targetMarks.asSequence().filter { it in relevantMarks }.forEach { mark -> + val state = MethodTaintMarkState(targetMethod, mark) + if (reachable.add(state)) pending.addLast(state) + } + + fun enqueue(method: Method, mark: String) { + if (mark !in relevantMarks) return + val state = MethodTaintMarkState(method, mark) + if (reachable.add(state)) pending.addLast(state) + } + + while (pending.isNotEmpty()) { + val (method, mark) = pending.removeFirst() + val summary = summaries[method] + + summary?.transitions?.forEach { transition -> + if (transition.outputMark == mark) enqueue(method, transition.inputMark) + } + + reverseRuleTransitions[method]?.get(mark).orEmpty().forEach { inputMark -> + enqueue(method, inputMark) + } + + if (mark in summary?.inputMarks.orEmpty()) { + callers[method].orEmpty().forEach { caller -> enqueue(caller, mark) } + } + + callees[method].orEmpty().forEach { callee -> + if (mark in summaries[callee]?.outputMarks.orEmpty()) { + enqueue(callee, mark) + } + } + } + + return reachable + } + + fun stats(): MethodTaintMarkSummaryStats { + var transitions = 0 + summaries.values.forEach { summary -> + transitions += summary.transitions.size + } + return MethodTaintMarkSummaryStats(summaries.size, transitions) + } + + private fun recordTransition(method: Method, inputMarks: Set, outputMarks: Set) { + if (inputMarks.isEmpty() && outputMarks.isEmpty()) return + + val summary = summaries.computeIfAbsent(method) { MethodSummary() } + summary.inputMarks += inputMarks + summary.outputMarks += outputMarks + inputMarks.forEach { inputMark -> + outputMarks.forEach { outputMark -> + summary.transitions += TaintMarkTransition(inputMark, outputMark) + } + } + } + + private fun recordOutput(method: Method, outputMarks: Set) { + if (outputMarks.isEmpty()) return + summaries.computeIfAbsent(method) { MethodSummary() }.outputMarks += outputMarks + } + + private fun FactAp.taintMarks(): Set = + getAllAccessors().filterIsInstanceTo(hashSetOf()).mapTo(hashSetOf()) { it.mark } + + internal fun recordExactSummary(method: Method, inputMark: String, outputMark: String) = + recordTransition(method, setOf(inputMark), setOf(outputMark)) + + internal fun recordSummary(method: Method, inputMarks: Set, outputMarks: Set) = + recordTransition(method, inputMarks, outputMarks) + + internal fun recordInputMark(method: Method, mark: String) { + summaries.computeIfAbsent(method) { MethodSummary() }.inputMarks += mark + } + + internal fun recordOutputMark(method: Method, mark: String) { + summaries.computeIfAbsent(method) { MethodSummary() }.outputMarks += mark + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt index 8563d1702..a72174924 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt @@ -12,6 +12,7 @@ import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ap.ifds.access.MethodAccessPathSubscription +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager import org.opentaint.dataflow.ap.ifds.serialization.MethodEntryPointSummaries import org.opentaint.dataflow.util.collectToListWithPostProcess import org.opentaint.dataflow.util.concurrentReadSafeForEach @@ -565,9 +566,13 @@ class SummaryEdgeSubscriptionManager( handleF2F: MethodAnalyzer.(List, List) -> Unit, handleZ2F: MethodAnalyzer.(List, List) -> Unit, handleND2F: MethodAnalyzer.(List, List) -> Unit, + emptyDeltaRequired: Boolean = false, ) { - subscriptionStorage.findFactEdgeSub(summaryInitialFact).forEach { (ep, subscriptions) -> - val summarySubs = subscriptions.mapTo(mutableListOf()) { + subscriptionStorage.findFactEdgeSub(summaryInitialFact, emptyDeltaRequired).forEach { (ep, subscriptions) -> + val summarySubs = subscriptions.mapNotNullTo(mutableListOf()) { + if (emptyDeltaRequired && !it.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) { + return@mapNotNullTo null + } FactToFactSub(it.callerPathEdge, it.calleeInitialFactBase) } @@ -578,7 +583,10 @@ class SummaryEdgeSubscriptionManager( } subscriptionStorage.findZeroEdgeSub(summaryInitialFact).forEach { (ep, subscriptions) -> - val summarySubs = subscriptions.mapTo(mutableListOf()) { + val summarySubs = subscriptions.mapNotNullTo(mutableListOf()) { + if (emptyDeltaRequired && !it.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) { + return@mapNotNullTo null + } ZeroToFactSub(it.callerPathEdge, it.calleeInitialFactBase) } @@ -588,8 +596,11 @@ class SummaryEdgeSubscriptionManager( analyzer.handleZ2F(summarySubs, summaries) } - subscriptionStorage.findFactNDEdgeSub(summaryInitialFact).forEach { (ep, subscriptions) -> - val summarySubs = subscriptions.mapTo(mutableListOf()) { + subscriptionStorage.findFactNDEdgeSub(summaryInitialFact, emptyDeltaRequired).forEach { (ep, subscriptions) -> + val summarySubs = subscriptions.mapNotNullTo(mutableListOf()) { + if (emptyDeltaRequired && !it.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) { + return@mapNotNullTo null + } NDFactToFactSub(it.callerPathEdge, it.calleeInitialFactBase) } @@ -611,34 +622,116 @@ class SummaryEdgeSubscriptionManager( } } + if (manager.apManager !is BaseOnlyApManager) { + for ((summaryInitialFact, summaries) in sameInitialFactEdges) { + applySummaries( + subscriptionStorage, summaryInitialFact, summaries, + MethodAnalyzer::handleFactToFactMethodNDSummaryEdge, + MethodAnalyzer::handleZeroToFactMethodNDSummaryEdge, + MethodAnalyzer::handleNDFactToFactMethodNDSummaryEdge, + emptyDeltaRequired = true, + ) + } + return + } + + val factActivations = linkedMapOf< + MethodEntryPoint, + MutableMap>, + >() + val zeroActivations = linkedMapOf< + MethodEntryPoint, + MutableMap>, + >() + val ndActivations = linkedMapOf< + MethodEntryPoint, + MutableMap>, + >() + for ((summaryInitialFact, summaries) in sameInitialFactEdges) { - applySummaries( - subscriptionStorage, summaryInitialFact, summaries, - MethodAnalyzer::handleFactToFactMethodNDSummaryEdge, - MethodAnalyzer::handleZeroToFactMethodNDSummaryEdge, - MethodAnalyzer::handleNDFactToFactMethodNDSummaryEdge, - ) + subscriptionStorage.findFactEdgeSub(summaryInitialFact, emptyDeltaRequired = true) + .forEach { (ep, subscriptions) -> + val bySubscription = factActivations.getOrPut(ep, ::linkedMapOf) + subscriptions.forEach { subscription -> + if (subscription.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) { + val sub = FactToFactSub( + subscription.callerPathEdge, + subscription.calleeInitialFactBase, + ) + bySubscription.getOrPut(sub, ::linkedSetOf).addAll(summaries) + } + } + } + + subscriptionStorage.findZeroEdgeSub(summaryInitialFact) + .forEach { (ep, subscriptions) -> + val bySubscription = zeroActivations.getOrPut(ep, ::linkedMapOf) + subscriptions.forEach { subscription -> + if (subscription.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) { + val sub = ZeroToFactSub( + subscription.callerPathEdge, + subscription.calleeInitialFactBase, + ) + bySubscription.getOrPut(sub, ::linkedSetOf).addAll(summaries) + } + } + } + + subscriptionStorage.findFactNDEdgeSub(summaryInitialFact, emptyDeltaRequired = true) + .forEach { (ep, subscriptions) -> + val bySubscription = ndActivations.getOrPut(ep, ::linkedMapOf) + subscriptions.forEach { subscription -> + if (subscription.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) { + val sub = NDFactToFactSub( + subscription.callerPathEdge, + subscription.calleeInitialFactBase, + ) + bySubscription.getOrPut(sub, ::linkedSetOf).addAll(summaries) + } + } + } + } + + factActivations.forEach { (ep, bySubscription) -> + val analyzer = processingCtx.getMethodAnalyzer(ep) + bySubscription.forEach { (sub, summaries) -> + analyzer.handleFactToFactMethodNDSummaryEdge(listOf(sub), summaries.toList()) + } + } + zeroActivations.forEach { (ep, bySubscription) -> + val analyzer = processingCtx.getMethodAnalyzer(ep) + bySubscription.forEach { (sub, summaries) -> + analyzer.handleZeroToFactMethodNDSummaryEdge(listOf(sub), summaries.toList()) + } + } + ndActivations.forEach { (ep, bySubscription) -> + val analyzer = processingCtx.getMethodAnalyzer(ep) + bySubscription.forEach { (sub, summaries) -> + analyzer.handleNDFactToFactMethodNDSummaryEdge(listOf(sub), summaries.toList()) + } } } } private inner class NewSideEffectRequirementEvent( private val methodEntryPoint: MethodEntryPoint, - private val sideEffectRequirements: List + private val sideEffectRequirements: List, ) : SummaryEvent { override fun processMethodSummary() { val methodSubscriptions = methodSummarySubscriptions[methodEntryPoint] ?: return sideEffectRequirements.forEach { sideEffectRequirement -> - methodSubscriptions.findFactEdgeSub(sideEffectRequirement, emptyDeltaRequired = true).forEach { (ep, subscriptions) -> - val analyzer = processingCtx.getMethodAnalyzer(ep) - for (subscription in subscriptions) { - analyzer.handleMethodSideEffectRequirement( - subscription.callerPathEdge, subscription.calleeInitialFactBase, - listOf(sideEffectRequirement) - ) + methodSubscriptions.findFactEdgeSub(sideEffectRequirement, emptyDeltaRequired = true) + .forEach { (ep, subscriptions) -> + val analyzer = processingCtx.getMethodAnalyzer(ep) + for (subscription in subscriptions) { + analyzer.handleMethodSideEffectRequirement( + subscription.callerPathEdge, + subscription.calleeInitialFactBase, + listOf(sideEffectRequirement), + ) + } } - } } } } @@ -737,6 +830,7 @@ class SummaryEdgeSubscriptionManager( processingCtx.addSummaryEdgeEvent(NewSideEffectSummaryEvent(methodEntryPoint, sideEffects)) } } + } class SummaryEdgeStorageWithSubscribers( @@ -789,6 +883,7 @@ class SummaryEdgeStorageWithSubscribers( addFactToFactEdges(factToFactEdges, addedEdges) addNDFactToFactEdges(ndFactToFactEdges, addedEdges) + if (addedEdges.isEmpty()) return for (subscriber in subscribers) { subscriber.newSummaryEdges(addedEdges) } @@ -823,6 +918,7 @@ class SummaryEdgeStorageWithSubscribers( fun sideEffectRequirement(requirements: List) { val addedRequirements = sideEffectRequirement.add(requirements) + if (addedRequirements.isEmpty()) return for (subscriber in subscribers) { subscriber.newSideEffectRequirement(methodEntryPoint, addedRequirements) } @@ -851,6 +947,7 @@ class SummaryEdgeStorageWithSubscribers( val addedSideEffects = addedZeroSideEffects + addedFactSideEffects + if (addedSideEffects.isEmpty()) return for (subscriber in subscribers) { subscriber.newSideEffectSummaries(methodEntryPoint, addedSideEffects) } @@ -959,6 +1056,13 @@ class SummaryEdgeStorageWithSubscribers( it.setEntryPoint(methodEntryPoint).build() }) + fun factToFactEdges(finalFactPattern: FinalFactAp): List = + collectToListWithPostProcess(mutableListOf(), { + taintedFactSummaryEdges.filterEdgesByFinalTo(it, finalFactPattern) + }, { + it.setEntryPoint(methodEntryPoint).build() + }) + fun factNDEdges(finalFactBase: AccessPathBase): List = collectToListWithPostProcess(mutableListOf(), { ndF2FSummaryEdges.filterEdgesTo(it, initialFactPattern = null, finalFactBase) @@ -993,9 +1097,12 @@ class SummaryEdgeStorageWithSubscribers( collectAllZeroToFactSummariesTo(sourceEdges) val sourceSummaries = sourceEdges.sumOf { (it as? Edge.ZeroToFact)?.factAp?.size ?: 0 } - val passEdges = mutableListOf() - collectAllFactToFactSummariesTo(passEdges) - val passSummaries = passEdges.sumOf { it.factAp.size } + val passSummaries = taintedFactSummaryEdges.storageStats()?.finalFactSizeSum + ?: run { + val passEdges = mutableListOf() + collectAllFactToFactSummariesTo(passEdges) + passEdges.sumOf { it.factAp.size.toLong() } + } stats.stats(methodEntryPoint.method).sourceSummaries += sourceSummaries stats.stats(methodEntryPoint.method).passSummaries += passSummaries @@ -1169,6 +1276,12 @@ abstract class MethodSummaryEdgesForExitPoint, Stor } } + fun forEachStorage(body: (Storage) -> Unit) { + exitPointsStorage.concurrentReadSafeMapIndexed { _, storage -> + body(storage) + } + } + private inline fun processStorageEdges(dst: MutableList, storageEdges: (Storage, MutableList) -> Unit) { exitPointsStorage.concurrentReadSafeMapIndexed { idx, storage -> val exitPoint = exitPoints[idx] diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisManager.kt index 17813b0f4..36b4df26d 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisManager.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisManager.kt @@ -3,15 +3,32 @@ package org.opentaint.dataflow.ap.ifds import org.opentaint.dataflow.ap.ifds.analysis.AnalysisManager import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext import org.opentaint.dataflow.ap.ifds.analysis.MethodCallResolver +import org.opentaint.dataflow.ap.ifds.taint.ActionableRules import org.opentaint.dataflow.ap.ifds.taint.TaintAnalysisContext +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem import org.opentaint.ir.api.common.CommonMethod import org.opentaint.ir.api.common.cfg.CommonInst import org.opentaint.util.analysis.ApplicationGraph interface TaintAnalysisManager : AnalysisManager { + val supportsForwardActionableRuleFallback: Boolean + get() = false + + fun relevantForwardActionableRules( + rules: ActionableRules, + uncoveredSinkRules: Set, + ): ActionableRules = rules + + fun overApproximateMethodContext( + method: MethodWithContext, + contextIndependentFact: Boolean, + ): MethodWithContext = method + sealed interface Phase { data object Prescan : Phase - data object FullScan : Phase + data object ShallowScan : Phase + data class FullScan(val actionableRules: Map>>) : Phase } fun selectPhase(phase: Phase) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt index 8d792378f..5b342c4db 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt @@ -44,16 +44,21 @@ class TaintAnalysisUnitRunner( runner = this ) - private object EventComparator : Comparator { + internal object EventComparator : Comparator { override fun compare(o1: Any, o2: Any): Int { - // Non-MethodAnalyzer events go first, MethodAnalyzers are sorted by analyzerSteps in ascending order - val methodAnalyzer1 = o1 as? MethodAnalyzer val methodAnalyzer2 = o2 as? MethodAnalyzer if (methodAnalyzer1 === methodAnalyzer2) { return 0 } + + val zeroToZeroPriority1 = methodAnalyzer1?.containsUnprocessedZeroToZeroEdges == true + val zeroToZeroPriority2 = methodAnalyzer2?.containsUnprocessedZeroToZeroEdges == true + if (zeroToZeroPriority1 != zeroToZeroPriority2) { + return if (zeroToZeroPriority1) -1 else 1 + } + if (methodAnalyzer1 == null) { return -1 } @@ -91,6 +96,13 @@ class TaintAnalysisUnitRunner( override fun resetApManager(apManager: ApManager) { resetQueue() + loadedSummaries.clear() + methodSummariesSerializer = MethodSummariesSerializer( + summarySerializationContext, + analysisManager, + apManager + ) + internalMethodSummarySubscriptions = SummaryEdgeSubscriptionManager(manager, this) externalMethodSummarySubscriptions = SummaryEdgeSubscriptionManager(manager, this) @@ -111,7 +123,7 @@ class TaintAnalysisUnitRunner( private val eventsProcessed = LongAdder() private val eventsEnqueued = LongAdder() - private val methodSummariesSerializer = MethodSummariesSerializer( + private var methodSummariesSerializer = MethodSummariesSerializer( summarySerializationContext, analysisManager, apManager @@ -204,6 +216,7 @@ class TaintAnalysisUnitRunner( var processed = true when (event) { is MethodAnalyzer -> { + var processingZeroToZeroEdges = event.containsUnprocessedZeroToZeroEdges while (event.containsUnprocessedEdges && isActive) { if (steps++ > RUNNER_STEPS_QUANT) { processed = false @@ -212,6 +225,16 @@ class TaintAnalysisUnitRunner( } event.tabulationAlgorithmStep() + + if (processingZeroToZeroEdges && !event.containsUnprocessedZeroToZeroEdges) { + if (event.containsUnprocessedEdges) { + processed = false + eventPriorityQueue.add(event) + } + break + } + + processingZeroToZeroEdges = event.containsUnprocessedZeroToZeroEdges } } @@ -331,6 +354,12 @@ class TaintAnalysisUnitRunner( addUnprocessedEvent(analyzer) } + override fun reprioritizeMethodAnalyzer(analyzer: MethodAnalyzer) { + if (eventPriorityQueue.remove(analyzer)) { + eventPriorityQueue.add(analyzer) + } + } + data class MethodAnalysisDelayed(val analyzer: MethodAnalyzer) data object DelayedAnalysisResume @@ -476,10 +505,13 @@ class TaintAnalysisUnitRunner( } } - fun methodTraceResolver(methodEntryPoint: MethodEntryPoint): MethodTraceResolver { + fun methodTraceResolver( + methodEntryPoint: MethodEntryPoint, + traceResolutionActionHardLimit: Int? = null, + ): MethodTraceResolver { val methodRunners = methodAnalyzers(methodEntryPoint) val runner = methodRunners.getAnalyzer(methodEntryPoint) - return runner.methodTraceResolver() + return runner.methodTraceResolver(traceResolutionActionHardLimit) } fun resolveIntraProceduralForwardFullTrace( diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt index c07b6bb9f..b0c5e6455 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt @@ -21,9 +21,11 @@ import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext import org.opentaint.dataflow.ap.ifds.analysis.MethodCallResolver import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext import org.opentaint.dataflow.ap.ifds.taint.CommonTaintAnalysisContext +import org.opentaint.dataflow.ap.ifds.taint.ActionableRules import org.opentaint.dataflow.ap.ifds.taint.TaintAnalysisUnitStorage import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker.TaintVulnerability +import org.opentaint.dataflow.ap.ifds.trace.ExactProcessingTimeBudget import org.opentaint.dataflow.ap.ifds.trace.ParallelProcessingContext import org.opentaint.dataflow.ap.ifds.trace.TraceResolver import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityChecker @@ -31,12 +33,16 @@ import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityChecker.VerifiedVulnera import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityChecker.VulnerabilityVerificationStatus import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithInterproceduralTrace import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace +import org.opentaint.dataflow.ap.ifds.trace.action.ActionableRulesCollectionResult +import org.opentaint.dataflow.ap.ifds.trace.action.collectActionableRules import org.opentaint.dataflow.ap.ifds.trace.path.TracePathGenerationResult import org.opentaint.dataflow.ap.ifds.trace.path.TracePathResolveParams import org.opentaint.dataflow.ap.ifds.trace.path.generateTracePath import org.opentaint.dataflow.ifds.UnitResolver import org.opentaint.dataflow.ifds.UnitType import org.opentaint.dataflow.ifds.UnknownUnit +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem import org.opentaint.dataflow.util.Cancellation import org.opentaint.dataflow.util.MemoryManager import org.opentaint.dataflow.util.RefManager @@ -54,7 +60,7 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource class TaintAnalysisUnitRunnerManager( - val refManager: RefManager, + override val refManager: RefManager, override val cancellation: Cancellation, private val analysisManager: TaintAnalysisManager, val graph: ApplicationGraph, @@ -79,6 +85,7 @@ class TaintAnalysisUnitRunnerManager( private val runnerForUnit = ConcurrentHashMap() private val unitStorage = ConcurrentHashMap() private val methodDependencies = ConcurrentHashMap>() + private val methodTaintMarkReachability = MethodTaintMarkReachabilityIndex() private val runnerJobs = ConcurrentLinkedQueue() private var analysisCompletion = CompletableDeferred() @@ -126,6 +133,7 @@ class TaintAnalysisUnitRunnerManager( fun resetApManager(manager: ApManager) { this.activeApManager = manager + methodTaintMarkReachability.clearSummaries() runnerForUnit.elements().iterator().forEach { it.resetApManager(manager) } unitStorage.elements().iterator().forEach { it.resetApManager(manager) } @@ -213,6 +221,43 @@ class TaintAnalysisUnitRunnerManager( return vulnerabilities } + fun getForwardActionableRules(): ActionableRules { + val rules = hashMapOf< + CommonInst, + MutableMap>, + >() + unitStorage.values.forEach { it.collectForwardActionableRules(rules) } + return rules + } + + fun resolveVulnerabilityActionableRules( + vulnerabilities: List, + timeout: Duration, + cancellationTimeout: Duration, + exactTimeBudget: ExactProcessingTimeBudget? = null, + ): List { + if (vulnerabilities.isEmpty()) return emptyList() + + if (!timeout.isPositive()) { + updateFailureStatus(Status.TIMEOUT) + return vulnerabilities.map { ActionableRulesCollectionResult.Unprocessed } + } + + cancellation.activate() + + val traceResolverMemoryManager = MemoryManager(refManager, TRACE_GENERATION_MEMORY_THRESHOLD) { + cancellation.cancel() + updateFailureStatus(Status.OOM) + logger.error { "Running low on memory, stopping actionable rules resolution" } + } + + return traceResolverMemoryManager.runWithMemoryManager { + resolveTraceActionableRulesWithCancellation( + vulnerabilities, timeout, cancellationTimeout, exactTimeBudget, + ) + } + } + fun resolveVulnerabilityTraces( vulnerabilities: List, resolverParams: TracePathResolveParams, @@ -220,6 +265,12 @@ class TaintAnalysisUnitRunnerManager( cancellationTimeout: Duration ): List { if (vulnerabilities.isEmpty()) return emptyList() + + if (!timeout.isPositive()) { + updateFailureStatus(Status.TIMEOUT) + return vulnerabilities.map { VulnerabilityWithTrace(it.vulnerability, TracePathGenerationResult.Failure) } + } + cancellation.activate() val traceResolverMemoryManager = MemoryManager(refManager, TRACE_GENERATION_MEMORY_THRESHOLD) { @@ -240,9 +291,18 @@ class TaintAnalysisUnitRunnerManager( vulnerabilities: List, resolverParams: TraceResolver.Params, timeout: Duration, - cancellationTimeout: Duration + cancellationTimeout: Duration, + exactTimeBudget: ExactProcessingTimeBudget? = null, ): List { if (vulnerabilities.isEmpty()) return emptyList() + + if (!timeout.isPositive()) { + updateFailureStatus(Status.TIMEOUT) + return vulnerabilities.map { + VulnerabilityWithInterproceduralTrace(it, trace = null, traceResolutionCompleted = false) + } + } + cancellation.activate() val traceResolverMemoryManager = MemoryManager(refManager, TRACE_GENERATION_MEMORY_THRESHOLD) { @@ -253,7 +313,7 @@ class TaintAnalysisUnitRunnerManager( return traceResolverMemoryManager.runWithMemoryManager { resolveVulnerabilityTracesWithCancellation( - entryPoints, vulnerabilities, resolverParams, timeout, cancellationTimeout + entryPoints, vulnerabilities, resolverParams, timeout, cancellationTimeout, exactTimeBudget, ) } } @@ -264,6 +324,7 @@ class TaintAnalysisUnitRunnerManager( resolverParams: TraceResolver.Params, timeout: Duration, cancellationTimeout: Duration, + exactTimeBudget: ExactProcessingTimeBudget?, ): List { val traceResolver = TraceResolver(entryPoints, this, resolverParams, cancellation) @@ -272,24 +333,62 @@ class TaintAnalysisUnitRunnerManager( analyzerDispatcher, name = "Trace resolution", states ) { override fun processItem(item: TraceResolver.State): ProcessingResult { - val res = traceResolver.resolveTrace(item) + if (exactTimeBudget?.isExhausted(item.vulnerability) == true) { + reportExactTime(item.vulnerability, "trace_limit") + return ProcessingResult.Done(unprocessedTrace(item.vulnerability)) + } + + val measurement = exactTimeBudget?.measure( + item.vulnerability, + ExactProcessingTimeBudget.Stage.TRACE_RESOLUTION, + cancellation, + ) { operationCancellation -> + traceResolver.resolveTrace(item, operationCancellation::isActive) + } + val res = measurement?.value ?: traceResolver.resolveTrace(item) + if (measurement?.snapshot?.exhausted == true) { + reportExactTime(item.vulnerability, "trace_limit") + return ProcessingResult.Done(unprocessedTrace(item.vulnerability)) + } + return when (res) { is TraceResolver.TraceResolutionResult.InProgress -> { ProcessingResult.Running(res.state) } is TraceResolver.TraceResolutionResult.NoTrace -> { + reportExactTime(res.vulnerability, "no_trace") ProcessingResult.Done(VulnerabilityWithInterproceduralTrace(res.vulnerability, trace = null)) } is TraceResolver.TraceResolutionResult.Resolved -> { + reportExactTime(res.vulnerability, "trace_resolved") ProcessingResult.Done(VulnerabilityWithInterproceduralTrace(res.vulnerability, res.trace)) } } } - override fun createUnprocessed(item: TraceResolver.State): VulnerabilityWithInterproceduralTrace = - VulnerabilityWithInterproceduralTrace(item.vulnerability, trace = null) + private fun unprocessedTrace(vulnerability: TaintVulnerability) = + VulnerabilityWithInterproceduralTrace( + vulnerability, trace = null, traceResolutionCompleted = false, + ) + + private fun reportExactTime(vulnerability: TaintVulnerability, outcome: String) { + val snapshot = exactTimeBudget?.snapshot(vulnerability) ?: return + logger.debug { + "Exact shallow rule search time: stage=trace outcome=$outcome " + + "trace_ns=${snapshot.traceResolution.inWholeNanoseconds} " + + "rules_ns=${snapshot.ruleSearch.inWholeNanoseconds} " + + "total_ns=${snapshot.total.inWholeNanoseconds} " + + "limit_ns=${snapshot.limit.inWholeNanoseconds} " + + "rule=${vulnerability.ruleId} sink=${vulnerability.statement}" + } + } + + override fun createUnprocessed(item: TraceResolver.State): VulnerabilityWithInterproceduralTrace { + reportExactTime(item.vulnerability, "global_limit") + return unprocessedTrace(item.vulnerability) + } private var prevStats: MethodStats? = null @@ -350,12 +449,85 @@ class TaintAnalysisUnitRunnerManager( ) } + private fun resolveTraceActionableRulesWithCancellation( + vulnerabilities: List, + timeout: Duration, + cancellationTimeout: Duration, + exactTimeBudget: ExactProcessingTimeBudget?, + ): List { + val traceResolutionContext = object : ParallelProcessingContext( + analyzerDispatcher, name = "Trace actionable entries resolution", vulnerabilities + ) { + override fun processItem(item: VulnerabilityWithInterproceduralTrace): ProcessingResult { + if (!item.traceResolutionCompleted) { + reportExactTime(item, "trace_unprocessed") + return ProcessingResult.Done(ActionableRulesCollectionResult.Unprocessed) + } + if (exactTimeBudget?.isExhausted(item.vulnerability) == true) { + reportExactTime(item, "rule_limit") + return ProcessingResult.Done(ActionableRulesCollectionResult.Unprocessed) + } + + val measurement = exactTimeBudget?.measure( + item.vulnerability, + ExactProcessingTimeBudget.Stage.RULE_SEARCH, + cancellation, + ) { operationCancellation -> + collectActionableRules(item, operationCancellation) + } + val resolved = measurement?.value ?: collectActionableRules(item) + if (measurement?.snapshot?.exhausted == true) { + reportExactTime(item, "rule_limit") + return ProcessingResult.Done(ActionableRulesCollectionResult.Unprocessed) + } + + val result = if (resolved === ActionableRulesCollectionResult.Failed && !cancellation.isActive()) { + ActionableRulesCollectionResult.Unprocessed + } else { + resolved + } + reportExactTime(item, result::class.simpleName ?: "unknown") + return ProcessingResult.Done(result) + } + + private fun reportExactTime(item: VulnerabilityWithInterproceduralTrace, outcome: String) { + val snapshot = exactTimeBudget?.snapshot(item.vulnerability) ?: return + logger.debug { + "Exact shallow rule search time: stage=rules outcome=$outcome " + + "trace_ns=${snapshot.traceResolution.inWholeNanoseconds} " + + "rules_ns=${snapshot.ruleSearch.inWholeNanoseconds} " + + "total_ns=${snapshot.total.inWholeNanoseconds} " + + "limit_ns=${snapshot.limit.inWholeNanoseconds} " + + "rule=${item.vulnerability.ruleId} sink=${item.vulnerability.statement}" + } + } + + override fun createUnprocessed(item: VulnerabilityWithInterproceduralTrace): ActionableRulesCollectionResult { + reportExactTime(item, "global_limit") + return ActionableRulesCollectionResult.Unprocessed + } + + override fun reportStats() { + logger.info { reportMemoryUsage() } + } + } + + return traceResolutionContext.processAll( + progressScope, timeout, cancellationTimeout, cancellation + ) + } + fun confirmVulnerabilities( entryPoints: Set, vulnerabilities: List, timeout: Duration, cancellationTimeout: Duration ): List { + if (!timeout.isPositive()) { + updateFailureStatus(Status.TIMEOUT) + return vulnerabilities + } + cancellation.activate() val confirmed = mutableListOf() @@ -427,6 +599,20 @@ class TaintAnalysisUnitRunnerManager( fun methodCallers(method: CommonMethod): Set = methodDependencies[method].orEmpty() + fun methodsThatCanReach(method: CommonMethod): Set = + methodTaintMarkReachability.methodsThatCanReach(method) + + fun taintMarkStatesThatCanReach( + method: CommonMethod, + marks: Set, + ruleTransitions: Map>, + relevantMarks: Set, + ): Set> = + methodTaintMarkReachability.statesThatCanReach(method, marks, ruleTransitions, relevantMarks) + + fun methodTaintMarkSummaryStats(): MethodTaintMarkSummaryStats = + methodTaintMarkReachability.stats() + fun findUnitRunner(unit: UnitType): TaintAnalysisUnitRunner? { if (unit == UnknownUnit) return null return runnerForUnit[unit] @@ -535,6 +721,15 @@ class TaintAnalysisUnitRunnerManager( dependencies.add(unit) } + override fun registerResolvedMethodCall(caller: CommonMethod, callee: CommonMethod) { + methodTaintMarkReachability.addCall(caller, callee) + } + + override fun newSummaryEdges(methodEntryPoint: MethodEntryPoint, edges: List) { + super.newSummaryEdges(methodEntryPoint, edges) + methodTaintMarkReachability.addSummaryEdges(methodEntryPoint.method, edges) + } + override fun getOrCreateUnitRunner(unit: UnitType): AnalysisRunner? { return getOrSpawnUnitRunner(unit) } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt index 69606f10f..3176fa9fa 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt @@ -107,7 +107,34 @@ interface MethodEdgesFinalApSet { } interface MethodEdgesInitialToFinalApSet { - fun add(statement: CommonInst, initialAp: InitialFactAp, finalAp: FinalFactAp): Pair? + /** + * Adds an edge and returns the complete propagation delta. If insertion changes metadata + * shared by several stored finals, every affected final must be returned with that metadata. + * An empty list means that the represented edge set did not change. + */ + fun add( + statement: CommonInst, + initialAp: InitialFactAp, + finalAp: FinalFactAp, + ): List> + + /** + * Adds several exact premises with one conclusion without requiring callers to materialize + * one path-edge object per premise. The callback is still an exact propagation delta: an + * implementation may emit more than one conclusion for a premise when shared metadata changes. + */ + fun addAll( + statement: CommonInst, + initialAps: Iterable, + finalAp: FinalFactAp, + emitDelta: (InitialFactAp, FinalFactAp) -> Unit, + ) { + initialAps.forEach { initialAp -> + add(statement, initialAp, finalAp).forEach { (addedInitial, addedFinal) -> + emitDelta(addedInitial, addedFinal) + } + } + } fun collectApAtStatement(collection: MutableList>, statement: CommonInst) fun collectApAtStatement(collection: MutableList>, statement: CommonInst, finalFactPattern: InitialFactAp) fun collectApAtStatement(collection: MutableList, statement: CommonInst, initialAp: InitialFactAp, finalFactPattern: InitialFactAp) @@ -139,8 +166,17 @@ interface MethodFinalApSummariesStorage { interface MethodInitialToFinalApSummariesStorage { fun add(edges: List, added: MutableList) fun filterEdgesTo(dst: MutableList, initialFactPattern: FinalFactAp?, finalFactBase: AccessPathBase?) + fun storageStats(): InitialToFinalSummaryStorageStats? = null + fun filterEdgesByFinalTo(dst: MutableList, finalFactPattern: FinalFactAp) { + filterEdgesTo(dst, initialFactPattern = null, finalFactBase = finalFactPattern.base) + } } +data class InitialToFinalSummaryStorageStats( + val edgeCount: Long, + val finalFactSizeSum: Long, +) + interface MethodNDInitialToFinalApSummariesStorage { fun add(edges: List, added: MutableList) fun filterEdgesTo(dst: MutableList, initialFactPattern: FinalFactAp?, finalFactBase: AccessPathBase?) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApMode.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApMode.kt index 77cffcbb5..b452844c6 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApMode.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApMode.kt @@ -1,5 +1,5 @@ package org.opentaint.dataflow.ap.ifds.access enum class ApMode { - Tree, Cactus, Automata + Tree, Cactus, Automata, BaseOnly, BaseOnlyField } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt index 63c728e01..6bb5ada28 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt @@ -24,7 +24,7 @@ class MethodEdgesInitialToFinalAutomataApSet( statement: CommonInst, initialAp: InitialFactAp, finalAp: FinalFactAp - ): Pair? = + ): List> = add(statement, initialAp as AccessGraphInitialFactAp, finalAp as AccessGraphFinalFactAp) override fun collectApAtStatement( @@ -73,7 +73,7 @@ class MethodEdgesInitialToFinalAutomataApSet( statement: CommonInst, initialAp: AccessGraphInitialFactAp, finalAp: AccessGraphFinalFactAp - ): Pair? { + ): List> { check(initialAp.exclusions == finalAp.exclusions) val storage = this.storage @@ -81,14 +81,27 @@ class MethodEdgesInitialToFinalAutomataApSet( .getOrCreate(initialAp.access) val exclusion = initialAp.exclusions - val addedExclusion = storage.add(statement, finalAp.base, finalAp.access, exclusion) - - if (addedExclusion === exclusion) return initialAp to finalAp - if (addedExclusion == null) return null + val update = storage.add(statement, finalAp.base, finalAp.access, exclusion) + ?: return emptyList() + val addedInitial = if (update.exclusion === exclusion) { + initialAp + } else { + initialAp.replaceExclusions(update.exclusion) + } + val addedAccesses = if (update.reemitAll) { + mutableListOf().also { storage.collectAccesses(it, statement, finalAp.base) } + } else { + listOf(finalAp.access) + } - val newInitial = initialAp.replaceExclusions(addedExclusion) - val newFinal = finalAp.replaceExclusions(addedExclusion) - return newInitial to newFinal + return addedAccesses.map { access -> + val addedFinal = if (access === finalAp.access && update.exclusion === exclusion) { + finalAp + } else { + AccessGraphFinalFactAp(finalAp.base, access, update.exclusion) + } + addedInitial to addedFinal + } } override fun toString(): String = storage.toString() @@ -122,15 +135,25 @@ class MethodEdgesInitialToFinalAutomataApSet( maxInstIdx: Int, languageManager: LanguageManager ) { + data class Update(val exclusion: ExclusionSet, val reemitAll: Boolean) + private val factStorage = FinalFactBaseStorage(initialStatement, maxInstIdx, languageManager) - fun add(statement: CommonInst, finalBase: AccessPathBase, finalAg: AccessGraph, exclusion: ExclusionSet): ExclusionSet? { + fun add( + statement: CommonInst, + finalBase: AccessPathBase, + finalAg: AccessGraph, + exclusion: ExclusionSet, + ): Update? { val finalFactStorage = factStorage.getOrCreate(finalBase) val factUpdated = finalFactStorage.addFact(statement, finalAg) + val exclusionUpdate = finalFactStorage.addExclusion(statement, exclusion) + if (!factUpdated && !exclusionUpdate.changed) return null + return Update(exclusionUpdate.exclusion, reemitAll = exclusionUpdate.changed) + } - return finalFactStorage.addExclusion( - statement, exclusion, returnNullIfNotUpdated = !factUpdated - ) + fun collectAccesses(dst: MutableList, statement: CommonInst, finalBase: AccessPathBase) { + factStorage.find(finalBase)?.collectTo(dst, statement) } fun collectTo(collection: MutableList, statement: CommonInst, finalFactPattern: InitialFactAp?) { @@ -172,6 +195,8 @@ class MethodEdgesInitialToFinalAutomataApSet( maxInstIdx: Int, private val languageManager: LanguageManager ) { + data class ExclusionUpdate(val exclusion: ExclusionSet, val changed: Boolean) + private val finalFacts = AccessGraphSetArray.create(instructionStorageSize(maxInstIdx)) fun addFact(statement: CommonInst, final: AccessGraph): Boolean { @@ -195,26 +220,22 @@ class MethodEdgesInitialToFinalAutomataApSet( private val exclusions = arrayOfNulls(instructionStorageSize(maxInstIdx)) - fun addExclusion( - statement: CommonInst, - exclusion: ExclusionSet, - returnNullIfNotUpdated: Boolean - ): ExclusionSet? { + fun addExclusion(statement: CommonInst, exclusion: ExclusionSet): ExclusionUpdate { val exclusionIdx = instructionStorageIdx(statement, languageManager) val currentExclusion = exclusions[exclusionIdx] if (currentExclusion == null) { exclusions[exclusionIdx] = exclusion - return exclusion + return ExclusionUpdate(exclusion, changed = true) } val merged = currentExclusion.union(exclusion) if (merged === currentExclusion) { - return if (returnNullIfNotUpdated) null else merged + return ExclusionUpdate(merged, changed = false) } exclusions[exclusionIdx] = merged - return merged + return ExclusionUpdate(merged, changed = true) } fun exclusion(statement: CommonInst): ExclusionSet? { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccess.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccess.kt new file mode 100644 index 000000000..c3964ecf4 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccess.kt @@ -0,0 +1,239 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor + +typealias BaseOnlyAccess = Long + +const val NO_ACCESSOR: AccessorIdx = -1 +const val ABSTRACT_MARK: AccessorIdx = -2 +const val COLLAPSED_MARK: AccessorIdx = -3 + +const val BASE_ONLY_STATIC_BITS = 16 +const val BASE_ONLY_FIELD_BITS = 24 +const val BASE_ONLY_SUFFIX_BITS = 24 +const val BASE_ONLY_VALUE_ACCESSOR_STATE_BITS = 1 +const val BASE_ONLY_SUFFIX_VALUE_BITS = BASE_ONLY_SUFFIX_BITS - BASE_ONLY_VALUE_ACCESSOR_STATE_BITS + +const val BASE_ONLY_SUFFIX_SHIFT = 0 +const val BASE_ONLY_FIELD_SHIFT = BASE_ONLY_SUFFIX_BITS +const val BASE_ONLY_STATIC_SHIFT = BASE_ONLY_SUFFIX_BITS + BASE_ONLY_FIELD_BITS + +const val BASE_ONLY_STATIC_MASK = (1 shl BASE_ONLY_STATIC_BITS) - 1 +const val BASE_ONLY_FIELD_MASK = (1 shl BASE_ONLY_FIELD_BITS) - 1 +const val BASE_ONLY_SUFFIX_MASK = (1 shl BASE_ONLY_SUFFIX_BITS) - 1 +const val BASE_ONLY_SUFFIX_VALUE_MASK = (1 shl BASE_ONLY_SUFFIX_VALUE_BITS) - 1 +const val BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT = BASE_ONLY_SUFFIX_VALUE_BITS +const val BASE_ONLY_VALUE_ACCESSOR_STATE_MASK = (1 shl BASE_ONLY_VALUE_ACCESSOR_STATE_BITS) - 1 + +const val BASE_ONLY_BIAS = 3 + +/** + * How the semantic suffix is reached. [Value] encodes a preceding ValueAccessor; + * for a type suffix the same bit encodes its analogous TypeInfoGroupAccessor prefix. + */ +enum class BaseOnlyValueAccessorState(val encoded: Int) { + Normal(0), + Value(1); + + companion object { + fun decode(encoded: Int): BaseOnlyValueAccessorState = + entries.firstOrNull { it.encoded == encoded } + ?: throw IllegalArgumentException("Invalid BaseOnly value-accessor state: $encoded") + } +} + +fun packBaseOnlyAccess( + staticIdx: AccessorIdx, + fieldIdx: AccessorIdx, + suffixIdx: AccessorIdx, + valueAccessorState: BaseOnlyValueAccessorState = BaseOnlyValueAccessorState.Normal, +): BaseOnlyAccess { + require(fieldIdx != ANY_ACCESSOR_IDX) { "AnyAccessor is implicit in BaseOnly and cannot occupy the field slot" } + val s = staticIdx + BASE_ONLY_BIAS + val f = fieldIdx + BASE_ONLY_BIAS + val x = suffixIdx + BASE_ONLY_BIAS + require(s in 0..BASE_ONLY_STATIC_MASK) { "BaseOnly static index out of range: $staticIdx" } + require(f in 0..BASE_ONLY_FIELD_MASK) { "BaseOnly field index out of range: $fieldIdx" } + require(x in 0..BASE_ONLY_SUFFIX_VALUE_MASK) { "BaseOnly suffix index out of range: $suffixIdx" } + val encodedSuffix = rawBaseOnlySuffixSlot(suffixIdx, valueAccessorState) + return (s.toLong() shl BASE_ONLY_STATIC_SHIFT) or + (f.toLong() shl BASE_ONLY_FIELD_SHIFT) or encodedSuffix.toLong() +} + +fun rawBaseOnlySuffixSlot(suffixIdx: AccessorIdx, valueAccessorState: BaseOnlyValueAccessorState): Int { + val encodedSuffix = suffixIdx + BASE_ONLY_BIAS + require(encodedSuffix in 0..BASE_ONLY_SUFFIX_VALUE_MASK) { + "BaseOnly suffix index out of range: $suffixIdx" + } + return encodedSuffix or (valueAccessorState.encoded shl BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT) +} + +fun packBaseOnlyAccessFromRawSuffix( + staticIdx: AccessorIdx, + fieldIdx: AccessorIdx, + rawSuffixSlot: Int, +): BaseOnlyAccess { + val suffixIdx = (rawSuffixSlot and BASE_ONLY_SUFFIX_VALUE_MASK) - BASE_ONLY_BIAS + val state = BaseOnlyValueAccessorState.decode( + (rawSuffixSlot ushr BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT) and BASE_ONLY_VALUE_ACCESSOR_STATE_MASK + ) + return packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, state) +} + +val EMPTY_ACCESS: BaseOnlyAccess = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, NO_ACCESSOR) +val ABSTRACT_EMPTY_ACCESS: BaseOnlyAccess = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK) +val FINAL_ACCESS: BaseOnlyAccess = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, FINAL_ACCESSOR_IDX) + +inline fun BaseOnlyAccess.withBaseOnlyAccessUnpacked( + body: (staticIdx: AccessorIdx, fieldIdx: AccessorIdx, suffixIdx: AccessorIdx) -> T, +): T = body( + ((this ushr BASE_ONLY_STATIC_SHIFT).toInt() and BASE_ONLY_STATIC_MASK) - BASE_ONLY_BIAS, + ((this ushr BASE_ONLY_FIELD_SHIFT).toInt() and BASE_ONLY_FIELD_MASK) - BASE_ONLY_BIAS, + (this.toInt() and BASE_ONLY_SUFFIX_VALUE_MASK) - BASE_ONLY_BIAS, +) + +val BaseOnlyAccess.staticIdx: AccessorIdx + get() = ((this ushr BASE_ONLY_STATIC_SHIFT).toInt() and BASE_ONLY_STATIC_MASK) - BASE_ONLY_BIAS + +val BaseOnlyAccess.fieldIdx: AccessorIdx + get() = ((this ushr BASE_ONLY_FIELD_SHIFT).toInt() and BASE_ONLY_FIELD_MASK) - BASE_ONLY_BIAS + +val BaseOnlyAccess.suffixIdx: AccessorIdx + get() = (this.toInt() and BASE_ONLY_SUFFIX_VALUE_MASK) - BASE_ONLY_BIAS + +val BaseOnlyAccess.rawSuffixSlot: Int + get() = this.toInt() and BASE_ONLY_SUFFIX_MASK + +val BaseOnlyAccess.valueAccessorState: BaseOnlyValueAccessorState + get() = BaseOnlyValueAccessorState.decode( + (rawSuffixSlot ushr BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT) and BASE_ONLY_VALUE_ACCESSOR_STATE_MASK + ) + +fun BaseOnlyAccess.withValueAccessorState(state: BaseOnlyValueAccessorState): BaseOnlyAccess = + packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, state) + +val BaseOnlyAccess.isSuffixAbstract: Boolean get() = suffixIdx == ABSTRACT_MARK + +val BaseOnlyAccess.isCollapsed: Boolean get() = suffixIdx == COLLAPSED_MARK + +val BaseOnlyAccess.apSlot: Int + get() = withBaseOnlyAccessUnpacked { s, f, x -> + when { + s == ABSTRACT_MARK -> 0 + f == ABSTRACT_MARK -> 1 + x == ABSTRACT_MARK -> 2 + else -> -1 + } + } + +val BaseOnlyAccess.hasAp: Boolean get() = apSlot >= 0 + +/** Whether abstract acceptance is available at the current logical node. */ +val BaseOnlyAccess.isRootAbstract: Boolean + get() = hasAp && staticIdx < 0 && fieldIdx < 0 + +val BaseOnlyAccess.hasSemanticMark: Boolean get() = suffixIdx >= 0 && suffixIdx != FINAL_ACCESSOR_IDX + +val BaseOnlyAccess.hasTerminalAccessor: Boolean get() = suffixIdx >= 0 + +val BaseOnlyAccess.hasTypeInfoSuffix: Boolean get() = suffixIdx >= 0 && suffixIdx.isTypeInfoAccessor() + +val BaseOnlyAccess.size: Int + get() = withBaseOnlyAccessUnpacked { staticIdx, fieldIdx, suffixIdx -> + var result = 0 + if (staticIdx >= 0) result++ + if (fieldIdx >= 0) result++ + if (suffixIdx >= 0) result++ + result + } + +val BaseOnlyAccess.coreSize: Int + get() = withBaseOnlyAccessUnpacked { s, f, x -> + var n = 0 + if (s >= 0) n++ + if (f >= 0) n++ + if (x >= 0 && x != FINAL_ACCESSOR_IDX) n++ + n + } + +val BaseOnlyAccess.isEmpty: Boolean get() = this == EMPTY_ACCESS + +val BaseOnlyAccess.headOrNull: AccessorIdx? + get() = withBaseOnlyAccessUnpacked { s, f, x -> + when { + s >= 0 -> s + f >= 0 -> f + x >= 0 && x != FINAL_ACCESSOR_IDX -> x + x == FINAL_ACCESSOR_IDX -> FINAL_ACCESSOR_IDX + else -> null + } + } + +val BaseOnlyAccess.firstAccessorOrNull: AccessorIdx? + get() = withBaseOnlyAccessUnpacked { s, f, x -> + when { + s >= 0 -> s + f >= 0 -> f + x < 0 -> null + x == FINAL_ACCESSOR_IDX -> FINAL_ACCESSOR_IDX + x.isTypeInfoAccessor() && valueAccessorState == BaseOnlyValueAccessorState.Value -> TYPE_INFO_GROUP_ACCESSOR_IDX + else -> x + } + } + +fun BaseOnlyAccess.coreAt(position: Int): AccessorIdx = withBaseOnlyAccessUnpacked { s, f, x -> + var k = position + if (s >= 0) { if (k == 0) return@withBaseOnlyAccessUnpacked s; k-- } + if (f >= 0) { if (k == 0) return@withBaseOnlyAccessUnpacked f; k-- } + if (x >= 0 && x != FINAL_ACCESSOR_IDX) { if (k == 0) return@withBaseOnlyAccessUnpacked x; k-- } + NO_ACCESSOR +} + +fun BaseOnlyAccess.coreStartsWith(prefix: BaseOnlyAccess, prefixLen: Int): Boolean { + if (coreSize < prefixLen) return false + for (i in 0 until prefixLen) if (coreAt(i) != prefix.coreAt(i)) return false + return true +} + +inline fun BaseOnlyAccess.forEachAccessorIdx(action: (AccessorIdx) -> Unit) { + val s = staticIdx + val f = fieldIdx + val x = suffixIdx + if (s >= 0) action(s) + if (f >= 0) action(f) + if (x >= 0) { + if (x != FINAL_ACCESSOR_IDX) { + if (x.isTypeInfoAccessor() && valueAccessorState == BaseOnlyValueAccessorState.Value) { + action(TYPE_INFO_GROUP_ACCESSOR_IDX) + } + action(x) + } + action(FINAL_ACCESSOR_IDX) + } +} + +inline fun BaseOnlyAccess.forEachCoreIdx(action: (AccessorIdx) -> Unit) { + val s = staticIdx + val f = fieldIdx + val x = suffixIdx + if (s >= 0) action(s) + if (f >= 0) action(f) + if (x >= 0 && x != FINAL_ACCESSOR_IDX) action(x) +} + +fun AccessorIdx.isAnyIdx(): Boolean = this == ANY_ACCESSOR_IDX +fun AccessorIdx.isStructuralIdx(): Boolean = isFieldAccessor() || this == ELEMENT_ACCESSOR_IDX +fun AccessorIdx.isSuffixIdx(): Boolean = !isAnyIdx() && !isStructuralIdx() && !isStaticAccessor() + +class BaseOnlyMatch( + @JvmField val emptyDelta: Boolean, + @JvmField val hasSuffix: Boolean, + @JvmField val suffix: BaseOnlyAccess, +) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessOps.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessOps.kt new file mode 100644 index 000000000..d16926932 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessOps.kt @@ -0,0 +1,555 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor + +class BaseOnlySplit( + @JvmField val matched: BaseOnlyAccess, + @JvmField val delta: BaseOnlyAccess, +) + +object BaseOnlyAccessOps { + val empty: BaseOnlyAccess get() = EMPTY_ACCESS + val abstractEmpty: BaseOnlyAccess get() = ABSTRACT_EMPTY_ACCESS + val finalAccess: BaseOnlyAccess get() = FINAL_ACCESS + + /** Validate the representation boundary without assigning semantics to malformed packed values. */ + fun requireCanonical( + access: BaseOnlyAccess, + allowEmpty: Boolean = false, + allowTransientCollapsed: Boolean = false, + ): BaseOnlyAccess { + val staticIdx = access.staticIdx + val fieldIdx = access.fieldIdx + val suffixIdx = access.suffixIdx + val valueAccessorState = access.valueAccessorState + + require(staticIdx == NO_ACCESSOR || staticIdx == ABSTRACT_MARK || staticIdx.isStaticAccessor()) { + "Invalid BaseOnly static slot: $staticIdx" + } + require( + fieldIdx == NO_ACCESSOR || fieldIdx == ABSTRACT_MARK || + fieldIdx.isFieldAccessor() || fieldIdx == ELEMENT_ACCESSOR_IDX + ) { "Invalid BaseOnly structural slot: $fieldIdx" } + require( + suffixIdx == NO_ACCESSOR || suffixIdx == ABSTRACT_MARK || + (allowTransientCollapsed && suffixIdx == COLLAPSED_MARK) || suffixIdx == FINAL_ACCESSOR_IDX || + (suffixIdx >= 0 && !suffixIdx.isStaticAccessor() && !suffixIdx.isFieldAccessor() && + suffixIdx != ELEMENT_ACCESSOR_IDX && suffixIdx != ANY_ACCESSOR_IDX && + suffixIdx != TYPE_INFO_GROUP_ACCESSOR_IDX && suffixIdx != VALUE_ACCESSOR_IDX) + ) { "Invalid BaseOnly suffix slot: $suffixIdx" } + require(access.hasSemanticMark || valueAccessorState == BaseOnlyValueAccessorState.Normal) { + "A value accessor is only valid before a semantic suffix: $valueAccessorState" + } + require(allowTransientCollapsed || !access.isCollapsed) { + "Collapsed BaseOnly access is a transient flow-function value" + } + if (staticIdx == ABSTRACT_MARK) { + require(fieldIdx == NO_ACCESSOR && suffixIdx == NO_ACCESSOR) { + "Components after a static abstraction are forbidden" + } + } + if (fieldIdx == ABSTRACT_MARK) { + require(staticIdx >= 0 || staticIdx == NO_ACCESSOR) { "Invalid prefix before field abstraction" } + require(suffixIdx == NO_ACCESSOR) { "Components after a field abstraction are forbidden" } + } + if (!access.hasAp && (staticIdx >= 0 || fieldIdx >= 0)) { + require(suffixIdx != NO_ACCESSOR) { "A concrete BaseOnly prefix must terminate or abstract" } + } + if (!allowEmpty) require(!access.isEmpty) { "Empty BaseOnly access is not a fact" } + return access + } + + fun build(accessors: IntArray, isAbstract: Boolean): BaseOnlyAccess { + validateBuildGrammar(accessors) + var staticIdx = NO_ACCESSOR + var fieldIdx = NO_ACCESSOR + var semanticIdx = NO_ACCESSOR + var valueAccessorState = BaseOnlyValueAccessorState.Normal + var hasFinal = false + for (idx in accessors) { + when { + idx.isStaticAccessor() -> { + require(staticIdx == NO_ACCESSOR || staticIdx == idx) { + "Multiple static accessors in a BaseOnly path: $staticIdx, $idx" + } + if (staticIdx == NO_ACCESSOR) staticIdx = idx + } + idx.isFieldAccessor() || idx == ELEMENT_ACCESSOR_IDX -> { + // Canonical BaseOnly retains the outermost structural accessor. + if (fieldIdx == NO_ACCESSOR) fieldIdx = idx + } + idx == ANY_ACCESSOR_IDX -> Unit + idx == TYPE_INFO_GROUP_ACCESSOR_IDX -> valueAccessorState = BaseOnlyValueAccessorState.Value + idx == VALUE_ACCESSOR_IDX -> valueAccessorState = BaseOnlyValueAccessorState.Value + idx == FINAL_ACCESSOR_IDX -> hasFinal = true + else -> if (semanticIdx < 0) semanticIdx = idx + } + } + val suffixIdx = when { + semanticIdx >= 0 -> semanticIdx + hasFinal -> FINAL_ACCESSOR_IDX + isAbstract -> ABSTRACT_MARK + else -> NO_ACCESSOR + } + return packNormalized(staticIdx, fieldIdx, suffixIdx, valueAccessorState) + } + + /** Validate accessor order before projecting a well-formed linear path into three slots. */ + private fun validateBuildGrammar(accessors: IntArray) { + var staticSeen = false + var semanticSeen = false + var finalSeen = false + var expectType = false + var expectMark = false + accessors.forEachIndexed { position, idx -> + require(!finalSeen) { "Accessor after FinalAccessor at position $position: $idx" } + if (semanticSeen) { + require(idx == FINAL_ACCESSOR_IDX) { "Accessor after BaseOnly semantic terminal at position $position: $idx" } + finalSeen = true + return@forEachIndexed + } + when { + expectType -> { + require(idx.isTypeInfoAccessor()) { "TypeInfoGroupAccessor must be followed by a type accessor" } + expectType = false + semanticSeen = true + } + expectMark -> { + require(idx.isTaintMarkAccessor()) { "ValueAccessor must be followed by a taint mark" } + expectMark = false + semanticSeen = true + } + idx.isStaticAccessor() -> { + require(position == 0 && !staticSeen) { "Static accessor is only valid once at the path root" } + staticSeen = true + } + structural(idx) -> Unit + idx == TYPE_INFO_GROUP_ACCESSOR_IDX -> expectType = true + idx == VALUE_ACCESSOR_IDX -> expectMark = true + idx == FINAL_ACCESSOR_IDX -> finalSeen = true + else -> semanticSeen = true // taint mark or compact type residual + } + } + require(!expectType) { "TypeInfoGroupAccessor requires a following type accessor" } + require(!expectMark) { "ValueAccessor requires a following taint mark" } + } + + fun abstractAt(staticIdx: AccessorIdx, fieldIdx: AccessorIdx, apSlot: Int): BaseOnlyAccess { + require(apSlot in 0..2) { "Invalid BaseOnly abstraction slot: $apSlot" } + return when (apSlot) { + 0 -> packNormalized(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR) + 1 -> packNormalized(staticIdx, ABSTRACT_MARK, NO_ACCESSOR) + else -> packNormalized(staticIdx, fieldIdx, ABSTRACT_MARK) + } + } + + fun collapse(access: BaseOnlyAccess): BaseOnlyAccess = when (access.apSlot) { + 0 -> packNormalized(NO_ACCESSOR, access.fieldIdx, access.suffixIdx, access.valueAccessorState) + 1 -> packNormalized(access.staticIdx, NO_ACCESSOR, access.suffixIdx, access.valueAccessorState) + 2 -> packNormalized(access.staticIdx, access.fieldIdx, COLLAPSED_MARK) + else -> access + } + + fun restoreAbstraction(access: BaseOnlyAccess): BaseOnlyAccess = + if (access.suffixIdx == COLLAPSED_MARK) packNormalized(access.staticIdx, access.fieldIdx, ABSTRACT_MARK) + else access + + fun prepend(access: BaseOnlyAccess, idx: AccessorIdx, fieldSensitive: Boolean): BaseOnlyAccess = when { + idx == TYPE_INFO_GROUP_ACCESSOR_IDX -> { + require(access.hasTypeInfoSuffix) { "TypeInfoGroupAccessor requires a compact type suffix" } + access.withValueAccessorState(BaseOnlyValueAccessorState.Value) + } + idx.isStaticAccessor() -> { + require(access.staticIdx == NO_ACCESSOR) { "Cannot prepend a second static accessor" } + packNormalized(idx, access.fieldIdx, access.suffixIdx, access.valueAccessorState) + } + idx.isAnyIdx() -> access + structural(idx) -> + if (!fieldSensitive) access + else packNormalized(access.staticIdx, idx, access.suffixIdx, access.valueAccessorState) + idx == VALUE_ACCESSOR_IDX -> { + require(access.hasSemanticMark && access.suffixIdx.isTaintMarkAccessor()) { + "ValueAccessor requires a taint-mark suffix" + } + access.withValueAccessorState(BaseOnlyValueAccessorState.Value) + } + else -> packNormalized(access.staticIdx, access.fieldIdx, idx, BaseOnlyValueAccessorState.Normal) + } + + fun read(access: BaseOnlyAccess, idx: AccessorIdx): BaseOnlyAccess? = when (headRead(access, idx)) { + HeadRead.NONE -> null + HeadRead.KEEP -> access + HeadRead.TAIL -> tail(access) + HeadRead.WRAPPER_TAIL -> wrapperTail(access) + } + + fun startsWith(access: BaseOnlyAccess, idx: AccessorIdx): Boolean = headRead(access, idx) != HeadRead.NONE + + fun clear(access: BaseOnlyAccess, idx: AccessorIdx): BaseOnlyAccess? { + if (access.staticIdx == NO_ACCESSOR && access.fieldIdx == NO_ACCESSOR && access.hasSemanticMark) { + // The missing field slot includes the implicit Any self-loop. Clearing a terminal + // root can remove the zero-length branch, but the same terminal remains reachable after + // one or more structural reads, so the BaseOnly projection is unchanged. + return access + } + + val head = access.firstAccessorOrNull ?: return access + if (head != idx) return access + + return null + } + + fun append(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): BaseOnlyAccess? { + if (suffix.isEmpty) return prefix + if (prefix.isEmpty) return suffix + if (prefix.hasAp) return graftAtAbstraction(prefix, suffix) + if (prefix.hasTerminalAccessor) return prefix + if (suffix.staticIdx >= 0 && prefix.coreSize > 0) return null + val prefixStaticConcrete = if (prefix.staticIdx == ABSTRACT_MARK) NO_ACCESSOR else prefix.staticIdx + val prefixFieldConcrete = if (prefix.fieldIdx == ABSTRACT_MARK) NO_ACCESSOR else prefix.fieldIdx + val staticIdx = if (suffix.staticIdx >= 0) suffix.staticIdx else prefixStaticConcrete + val fieldIdx = when { + prefixFieldConcrete >= 0 -> prefixFieldConcrete + suffix.fieldIdx != NO_ACCESSOR -> suffix.fieldIdx + else -> NO_ACCESSOR + } + val suffixIdx = + if (fieldIdx == ABSTRACT_MARK) NO_ACCESSOR + else combineTerminal(prefix, suffix) + val valueAccessorState = when { + prefix.hasSemanticMark -> prefix.valueAccessorState + suffix.hasSemanticMark -> suffix.valueAccessorState + else -> BaseOnlyValueAccessorState.Normal + } + return packNormalized(staticIdx, fieldIdx, suffixIdx, valueAccessorState) + } + + fun appendFinal(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): BaseOnlyAccess? { + if (suffix.isEmpty) return prefix + if (!prefix.hasAp) return null + return graftAtAbstraction(prefix, suffix) + } + + /** + * Graft [suffix] at [prefix]'s abstract accepting node. A suffix that starts in a later + * representational category is valid: loss of an intermediate field is widened with symbolic + * Any when an exact or semantic terminal follows. Only a second static is structurally + * impossible. + */ + private fun graftAtAbstraction(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): BaseOnlyAccess? { + return when (prefix.apSlot) { + 0 -> suffix + 1 -> { + if (suffix.staticIdx != NO_ACCESSOR) return null + packNormalized(prefix.staticIdx, suffix.fieldIdx, suffix.suffixIdx, suffix.valueAccessorState) + } + 2 -> { + if (suffix.staticIdx != NO_ACCESSOR) return null + // The prefix's suffix abstraction already contains an implicit Any step. It is + // the earlier structural step even when no concrete prefix field is retained, so + // a structural suffix is absorbed rather than installed into the empty field slot. + // Keeping only the incoming semantic terminal covers both the zero-length and + // structural branches represented by the prefix. + val field = prefix.fieldIdx + val terminal = when { + suffix.fieldIdx != NO_ACCESSOR && !suffix.hasSemanticMark -> ABSTRACT_MARK + else -> suffix.suffixIdx + } + packNormalized(prefix.staticIdx, field, terminal, suffix.valueAccessorState) + } + else -> null + } + } + + private fun slotVal(a: BaseOnlyAccess, slot: Int): AccessorIdx = when (slot) { + 0 -> a.staticIdx + 1 -> a.fieldIdx + else -> a.suffixIdx + } + + private fun matchesInitialPrefix(pattern: BaseOnlyAccess, x: BaseOnlyAccess): Boolean { + if (pattern == x) return true + if (!pattern.hasAp) return false + val k = pattern.apSlot + for (j in 0 until k) { + val patternSlot = slotVal(pattern, j) + val factSlot = slotVal(x, j) + val matches = if (j == 1) fieldCovers(patternSlot, factSlot, pattern) else patternSlot == factSlot + if (!matches) return false + } + if (slotVal(x, k) == NO_ACCESSOR) return false + if (x.hasAp && x.apSlot < k) return false + return true + } + + fun matchPrefix(final: BaseOnlyAccess, initial: BaseOnlyAccess): BaseOnlyMatch { + if (final == initial) return IDENTITY_MATCH + if (!matchesInitialPrefix(initial, final)) return NO_MATCH + return BaseOnlyMatch(emptyDelta = false, hasSuffix = true, suffix = dropCorePrefix(final, initial.apSlot)) + } + + fun splitConcreteInitial(final: BaseOnlyAccess, initial: BaseOnlyAccess): BaseOnlySplit? { + if (initial.hasAp) return null + return when (final.apSlot) { + 0 -> BaseOnlySplit(final, initial) + 1 -> { + if (!staticsCompatible(initial.staticIdx, final.staticIdx)) return null + BaseOnlySplit( + final, + packNormalized(NO_ACCESSOR, initial.fieldIdx, initial.suffixIdx, initial.valueAccessorState), + ) + } + 2 -> { + if (!staticsCompatible(initial.staticIdx, final.staticIdx)) return null + if (!fieldsCompatible(initial.fieldIdx, final.fieldIdx)) return null + BaseOnlySplit( + final, + packNormalized(NO_ACCESSOR, NO_ACCESSOR, initial.suffixIdx, initial.valueAccessorState), + ) + } + else -> null + } + } + + fun splitDelta( + fact: BaseOnlyAccess, + pattern: BaseOnlyAccess, + manager: BaseOnlyApManager, + exclusions: ExclusionSet, + ): List> { + if (fact.hasAp) { + if (!containsAccess(pattern, fact)) return emptyList() + + // A suffix-abstract fact matched by a field-abstract summary still has a suffix + // beyond the matched field slot. Preserve it so mapping the summary initial and + // concatenating the delta reconstructs the caller fact. In particular: + // matched against field.* retains field.*; and + // matched against .* retains *. + if (pattern.apSlot == 1 && fact.apSlot == 2) { + val delta = dropCorePrefix(fact, pattern.apSlot) + val filtered = manager.applyExclusions(delta, exclusions) ?: return emptyList() + return listOf(pattern to BaseOnlyNodeInitialDelta(manager, filtered)) + } + + return listOf(pattern to BaseOnlyEmptyInitialDelta) + } + + if (pattern.hasAp) { + val split = splitConcreteInitial(pattern, fact) ?: return emptyList() + // A field-lenient match may align `knownField.*` with a root-level suffix after + // projection erased one structural side. The summary exclusion is scoped after the + // known field and therefore must not be applied to that root-level residual. + val erasedStructuralBoundary = pattern.apSlot == 2 && + ((pattern.fieldIdx == NO_ACCESSOR) != (fact.fieldIdx == NO_ACCESSOR)) + val filtered = + if (erasedStructuralBoundary) split.delta + else manager.applyExclusions(split.delta, exclusions) ?: return emptyList() + return listOf(split.matched to BaseOnlyNodeInitialDelta(manager, filtered)) + } + + if (containsAccess(pattern, fact)) { + return listOf(pattern to BaseOnlyEmptyInitialDelta) + } + return emptyList() + } + + /** Directional logical coverage: every path in [fact] is represented by [pattern]. */ + fun covers(pattern: BaseOnlyAccess, fact: BaseOnlyAccess): Boolean { + if (pattern == fact) return true + + if (pattern.staticIdx == ABSTRACT_MARK) return true + if (fact.staticIdx == ABSTRACT_MARK) return false + if (!staticsCompatible(pattern.staticIdx, fact.staticIdx)) return false + + if (pattern.fieldIdx == ABSTRACT_MARK) return true + if (fact.fieldIdx == ABSTRACT_MARK) return false + if (!fieldCovers(pattern.fieldIdx, fact.fieldIdx, pattern)) return false + + if (pattern.suffixIdx == ABSTRACT_MARK) return true + if (fact.suffixIdx == ABSTRACT_MARK) return false + if (pattern.suffixIdx == NO_ACCESSOR) return false + if (pattern.suffixIdx != fact.suffixIdx) return false + return !pattern.hasSemanticMark || pattern.valueAccessorState == fact.valueAccessorState + } + + /** Symmetric candidate relation. It is deliberately distinct from directional [covers]. */ + fun mayOverlap(left: BaseOnlyAccess, right: BaseOnlyAccess): Boolean { + if (left == right) return true + if (left.staticIdx == ABSTRACT_MARK || right.staticIdx == ABSTRACT_MARK) return true + if (!staticsCompatible(left.staticIdx, right.staticIdx)) return false + + if (left.fieldIdx == ABSTRACT_MARK || right.fieldIdx == ABSTRACT_MARK) return true + if (left.fieldIdx >= 0 && right.fieldIdx >= 0 && left.fieldIdx != right.fieldIdx) return false + if (left.fieldIdx >= 0 && right.fieldIdx == NO_ACCESSOR && !hasVirtualStructuralAny(right) + ) return false + if (right.fieldIdx >= 0 && left.fieldIdx == NO_ACCESSOR && !hasVirtualStructuralAny(left) + ) return false + + if (left.suffixIdx == ABSTRACT_MARK || right.suffixIdx == ABSTRACT_MARK) return true + if (left.suffixIdx == NO_ACCESSOR || right.suffixIdx == NO_ACCESSOR) return false + if (left.suffixIdx != right.suffixIdx) return false + return !left.hasSemanticMark || left.valueAccessorState == right.valueAccessorState + } + + /** + * Projected final-to-initial containment. + * + * A missing structural slot is compatible with a concrete structural slot here because + * BaseOnly projection erases intermediate fields. This relation is intentionally broader + * than directional [covers]: it implements the cross-domain `FinalFactAp.contains` + * contract, not storage subsumption. + */ + fun containsAccess(final: BaseOnlyAccess, initial: BaseOnlyAccess): Boolean { + if (final == initial) return true + + if (final.staticIdx == ABSTRACT_MARK) return true + if (!staticsCompatible(final.staticIdx, initial.staticIdx)) return false + + if (final.fieldIdx == ABSTRACT_MARK) return true + if (!fieldsCompatible(final.fieldIdx, initial.fieldIdx)) return false + + if (final.suffixIdx == ABSTRACT_MARK) return true + if (final.suffixIdx == NO_ACCESSOR) return false + if (final.suffixIdx != initial.suffixIdx) return false + return !final.hasSemanticMark || final.valueAccessorState == initial.valueAccessorState + } + + /** + * The first concrete accessor selected by [candidate] after [pattern]'s abstraction point. + * A concrete structural slot in the candidate is residual when the suffix-abstract pattern + * has no corresponding structural slot: BaseOnly's implicit Any step crosses that boundary. + */ + fun firstAccessorAfterAbstraction( + pattern: BaseOnlyAccess, + candidate: BaseOnlyAccess, + ): AccessorIdx? = when (pattern.apSlot) { + 0 -> candidate.staticIdx.takeIf { it >= 0 } + ?: candidate.fieldIdx.takeIf { it >= 0 } + ?: candidate.suffixIdx.takeIf { it >= 0 } + + 1 -> candidate.fieldIdx.takeIf { it >= 0 } + ?: candidate.suffixIdx.takeIf { it >= 0 } + + 2 -> when { + pattern.fieldIdx == NO_ACCESSOR && candidate.fieldIdx >= 0 -> candidate.fieldIdx + else -> candidate.suffixIdx.takeIf { it >= 0 } + } + + else -> null + } + + fun equalToInitial(final: BaseOnlyAccess, initial: BaseOnlyAccess): Boolean { + if (initial.staticIdx != final.staticIdx) return false + if (initial.fieldIdx != final.fieldIdx) return false + val initialSemantic = if (initial.hasSemanticMark) initial.suffixIdx else NO_ACCESSOR + val finalSemantic = if (final.hasSemanticMark) final.suffixIdx else NO_ACCESSOR + if (initialSemantic != finalSemantic) return false + if (initialSemantic >= 0 && initial.valueAccessorState != final.valueAccessorState) return false + val terminalsAgree = + if (initial.hasTerminalAccessor) !final.isSuffixAbstract + else final.isSuffixAbstract == initial.isSuffixAbstract + return terminalsAgree + } + + private enum class HeadRead { NONE, KEEP, TAIL, WRAPPER_TAIL } + + private fun headRead(access: BaseOnlyAccess, idx: AccessorIdx): HeadRead { + if (access.staticIdx >= 0) return if (idx == access.staticIdx) HeadRead.TAIL else HeadRead.NONE + if (access.staticIdx == ABSTRACT_MARK) return HeadRead.NONE + if (access.fieldIdx >= 0) return if (idx == access.fieldIdx) HeadRead.TAIL else HeadRead.NONE + if (access.fieldIdx == ABSTRACT_MARK) return HeadRead.NONE + return when { + access.hasSemanticMark -> when { + structural(idx) -> HeadRead.KEEP + idx == terminalWrapperIdx(access) && access.valueAccessorState == BaseOnlyValueAccessorState.Value -> + HeadRead.WRAPPER_TAIL + idx == access.suffixIdx && access.valueAccessorState == BaseOnlyValueAccessorState.Normal -> HeadRead.TAIL + else -> HeadRead.NONE + } + access.suffixIdx == ABSTRACT_MARK -> if (structural(idx)) HeadRead.KEEP else HeadRead.NONE + access.isCollapsed -> if (structural(idx)) HeadRead.KEEP else HeadRead.NONE + access.suffixIdx == FINAL_ACCESSOR_IDX -> if (idx == FINAL_ACCESSOR_IDX) HeadRead.KEEP else HeadRead.NONE + else -> HeadRead.NONE + } + } + + private fun tail(access: BaseOnlyAccess): BaseOnlyAccess = when { + access.staticIdx >= 0 -> packNormalized( + NO_ACCESSOR, access.fieldIdx, access.suffixIdx, access.valueAccessorState + ) + access.fieldIdx >= 0 -> + packNormalized(NO_ACCESSOR, NO_ACCESSOR, access.suffixIdx, access.valueAccessorState) + access.hasSemanticMark -> packNormalized(NO_ACCESSOR, NO_ACCESSOR, FINAL_ACCESSOR_IDX) + else -> EMPTY_ACCESS + } + + private fun wrapperTail(access: BaseOnlyAccess): BaseOnlyAccess = + packNormalized(NO_ACCESSOR, NO_ACCESSOR, access.suffixIdx, BaseOnlyValueAccessorState.Normal) + + private fun combineTerminal(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): AccessorIdx = when { + prefix.hasSemanticMark -> prefix.suffixIdx + suffix.hasSemanticMark -> suffix.suffixIdx + suffix.suffixIdx == FINAL_ACCESSOR_IDX -> FINAL_ACCESSOR_IDX + suffix.suffixIdx == ABSTRACT_MARK -> ABSTRACT_MARK + prefix.suffixIdx == ABSTRACT_MARK -> ABSTRACT_MARK + else -> NO_ACCESSOR + } + + private fun dropCorePrefix(access: BaseOnlyAccess, dropSlots: Int): BaseOnlyAccess { + val staticIdx = if (dropSlots <= 0) access.staticIdx else NO_ACCESSOR + val fieldIdx = if (dropSlots <= 1) access.fieldIdx else NO_ACCESSOR + return packNormalized(staticIdx, fieldIdx, access.suffixIdx, access.valueAccessorState) + } + + private fun structural(idx: AccessorIdx): Boolean = idx.isStructuralIdx() || idx.isAnyIdx() + + private fun terminalWrapperIdx(access: BaseOnlyAccess): AccessorIdx = when { + access.hasTypeInfoSuffix -> TYPE_INFO_GROUP_ACCESSOR_IDX + access.suffixIdx.isTaintMarkAccessor() -> VALUE_ACCESSOR_IDX + else -> NO_ACCESSOR + } + + private fun hasVirtualStructuralAny(access: BaseOnlyAccess): Boolean = + access.fieldIdx == NO_ACCESSOR && + (access.hasSemanticMark || access.isSuffixAbstract || access.isCollapsed) + + private fun fieldCovers(patternField: AccessorIdx, factField: AccessorIdx, pattern: BaseOnlyAccess): Boolean = when { + patternField == factField -> true + patternField == NO_ACCESSOR -> factField >= 0 && hasVirtualStructuralAny(pattern) + else -> false + } + + private fun staticsCompatible(a: AccessorIdx, b: AccessorIdx): Boolean = a == b + + private fun fieldsCompatible(a: AccessorIdx, b: AccessorIdx): Boolean = + a == NO_ACCESSOR || b == NO_ACCESSOR || a == b + + private fun packNormalized( + staticIdx: AccessorIdx, + fieldIdx: AccessorIdx, + suffixIdx: AccessorIdx, + valueAccessorState: BaseOnlyValueAccessorState = BaseOnlyValueAccessorState.Normal, + ): BaseOnlyAccess { + val apEarlier = staticIdx == ABSTRACT_MARK || fieldIdx == ABSTRACT_MARK + val normalizedSuffix = + if (suffixIdx == NO_ACCESSOR && !apEarlier && (staticIdx >= 0 || fieldIdx >= 0)) ABSTRACT_MARK + else suffixIdx + val normalizedState = + if (normalizedSuffix >= 0 && normalizedSuffix != FINAL_ACCESSOR_IDX) valueAccessorState + else BaseOnlyValueAccessorState.Normal + return packBaseOnlyAccess(staticIdx, fieldIdx, normalizedSuffix, normalizedState) + } + + private val NO_MATCH = BaseOnlyMatch(emptyDelta = false, hasSuffix = false, suffix = EMPTY_ACCESS) + private val IDENTITY_MATCH = BaseOnlyMatch(emptyDelta = true, hasSuffix = false, suffix = EMPTY_ACCESS) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessView.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessView.kt new file mode 100644 index 000000000..dd1e358f1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessView.kt @@ -0,0 +1,65 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor + +fun BaseOnlyApManager.startsWithAccessor(access: BaseOnlyAccess, accessor: Accessor): Boolean = + BaseOnlyAccessOps.startsWith(access, interner.index(accessor)) + +fun BaseOnlyApManager.startAccessors(access: BaseOnlyAccess): Set { + val staticIdx = access.staticIdx + if (staticIdx >= 0) return setOf(accessor(staticIdx)) + if (staticIdx == ABSTRACT_MARK) return emptySet() + + val fieldIdx = access.fieldIdx + if (fieldIdx >= 0) { + return setOf(accessor(fieldIdx)) + } + if (fieldIdx == ABSTRACT_MARK) return emptySet() + + return when { + access.hasTypeInfoSuffix -> terminalStarts( + access, + TypeInfoGroupAccessor, + accessor(access.suffixIdx), + ) + AnyAccessor + access.hasSemanticMark && access.suffixIdx.isTaintMarkAccessor() -> terminalStarts( + access, + ValueAccessor, + accessor(access.suffixIdx), + ) + AnyAccessor + access.isSuffixAbstract || access.isCollapsed -> setOf(AnyAccessor) + access.hasSemanticMark -> setOf(AnyAccessor, accessor(access.suffixIdx)) + access.suffixIdx >= 0 -> setOf(accessor(access.suffixIdx)) + else -> emptySet() + } +} + +fun BaseOnlyApManager.allAccessors(access: BaseOnlyAccess): Set = + buildSet { + if (access.hasSemanticMark && access.suffixIdx.isTaintMarkAccessor() && + access.valueAccessorState == BaseOnlyValueAccessorState.Value + ) add(ValueAccessor) + access.forEachAccessorIdx { idx -> + val accessor = accessor(idx) + if (accessor != AnyAccessor) add(accessor) + } + } + +private fun terminalStarts( + access: BaseOnlyAccess, + wrapper: Accessor, + suffix: Accessor, +): Set = when (access.valueAccessorState) { + BaseOnlyValueAccessorState.Normal -> setOf(suffix) + BaseOnlyValueAccessorState.Value -> setOf(wrapper) +} + +fun BaseOnlyApManager.readAccess(access: BaseOnlyAccess, accessor: Accessor): BaseOnlyAccess? = + BaseOnlyAccessOps.read(access, interner.index(accessor)) + +private fun BaseOnlyApManager.accessor(idx: Int): Accessor = + interner.accessor(idx) ?: error("Accessor not found: $idx") diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApAccess.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApAccess.kt new file mode 100644 index 000000000..288dd82c1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApAccess.kt @@ -0,0 +1,30 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.common.FinalApAccess +import org.opentaint.dataflow.ap.ifds.access.common.InitialApAccess + +interface BaseOnlyFinalApAccess : FinalApAccess { + val apManager: BaseOnlyApManager + + override fun getFinalAccess(factAp: FinalFactAp): BaseOnlyAccess = + (factAp as BaseOnlyFinalFactAp).access + + override fun createFinal(base: AccessPathBase, ap: BaseOnlyAccess, ex: ExclusionSet): FinalFactAp = + BaseOnlyFinalFactAp(apManager, base, ap, ex) + +} + +interface BaseOnlyInitialApAccess : InitialApAccess { + val apManager: BaseOnlyApManager + + override fun getInitialAccess(factAp: InitialFactAp): BaseOnlyAccess = + (factAp as BaseOnlyInitialFactAp).access + + override fun createInitial(base: AccessPathBase, ap: BaseOnlyAccess, ex: ExclusionSet): InitialFactAp = + BaseOnlyInitialFactAp(apManager, base, ap, ex) + +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApManager.kt new file mode 100644 index 000000000..ef4bfb8fa --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApManager.kt @@ -0,0 +1,151 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.ExclusionSet.Empty +import org.opentaint.dataflow.ap.ifds.LanguageManager +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.ApManager +import org.opentaint.dataflow.ap.ifds.access.FactSideEffectSummariesApStorage +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.FinalFactList +import org.opentaint.dataflow.ap.ifds.access.InitialFactAbstraction +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.MethodAccessPathSubscription +import org.opentaint.dataflow.ap.ifds.access.MethodEdgesFinalApSet +import org.opentaint.dataflow.ap.ifds.access.MethodEdgesInitialToFinalApSet +import org.opentaint.dataflow.ap.ifds.access.MethodEdgesNDInitialToFinalApSet +import org.opentaint.dataflow.ap.ifds.access.MethodFinalApSummariesStorage +import org.opentaint.dataflow.ap.ifds.access.MethodInitialToFinalApSummariesStorage +import org.opentaint.dataflow.ap.ifds.access.MethodNDInitialToFinalApSummariesStorage +import org.opentaint.dataflow.ap.ifds.access.SideEffectRequirementApStorage +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor +import org.opentaint.dataflow.ap.ifds.serialization.ApSerializer +import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.ir.api.common.cfg.CommonInst + +class BaseOnlyApManager( + override val anyAccessorUnrollStrategy: AnyAccessorUnrollStrategy, + override val cancellation: Cancellation, + val fieldSensitive: Boolean = false, + val fieldGeneralizationEnabled: Boolean = false, + val summaryStorageFieldGeneralizationEnabled: Boolean = false, +) : ApManager { + val interner = AccessorInterner() + + @Volatile + private var traceResolutionMode = false + + /** One-way analyzer phase transition; individual queries capture the phase at entry. */ + fun enableTraceResolutionMode() { + traceResolutionMode = true + } + + fun traceResolutionModeEnabled(): Boolean = traceResolutionMode + + val Accessor.idx: AccessorIdx get() = interner.index(this) + + val AccessorIdx.accessor: Accessor + get() = interner.accessor(this) ?: error("Accessor not found: $this") + + val finalAccessorAccess: BaseOnlyAccess get() = FINAL_ACCESS + + override fun mostAbstractInitialAp(base: AccessPathBase): InitialFactAp = + BaseOnlyInitialFactAp(this, base, ABSTRACT_EMPTY_ACCESS, Empty) + + override fun mostAbstractFinalAp(base: AccessPathBase): FinalFactAp = + BaseOnlyFinalFactAp(this, base, ABSTRACT_EMPTY_ACCESS, Empty) + + override fun createFinalAp(base: AccessPathBase, exclusions: ExclusionSet): FinalFactAp = + BaseOnlyFinalFactAp(this, base, finalAccessorAccess, exclusions) + + override fun createFinalInitialAp(base: AccessPathBase, exclusions: ExclusionSet): InitialFactAp = + BaseOnlyInitialFactAp(this, base, finalAccessorAccess, exclusions) + + fun applyExclusions(suffix: BaseOnlyAccess, exclusions: ExclusionSet): BaseOnlyAccess? = + when (exclusions) { + ExclusionSet.Universe -> null + ExclusionSet.Empty -> suffix + is ExclusionSet.Concrete -> { + if (suffix.staticIdx == NO_ACCESSOR && suffix.fieldIdx == NO_ACCESSOR && suffix.hasSemanticMark) { + // The missing field slot carries the implicit Any self-loop. Exact subtraction + // is not representable, so retain the compact cover. + suffix + } else { + val head = suffix.firstAccessorOrNull + val accessor = head?.let(interner::accessor) + if (accessor == null) suffix else suffix.takeUnless { exclusions.contains(accessor) } + } + } + } + + fun renderAccess(access: BaseOnlyAccess): String { + val sb = StringBuilder() + access.forEachAccessorIdx { sb.append(idxToText(it)) } + if (access.isSuffixAbstract) sb.append(".*") + if (access.isCollapsed) sb.append(".^") + return sb.toString() + } + + private fun idxToText(idx: AccessorIdx): String = + interner.accessor(idx)?.toSuffix() ?: when { + idx.isAnyIdx() -> ".[any]" + idx == FINAL_ACCESSOR_IDX -> ".$" + idx.isStaticAccessor() -> "" + idx.isStructuralIdx() -> ".f#$idx" + else -> ".#$idx" + } + + override fun initialFactAbstraction(methodInitialStatement: CommonInst): InitialFactAbstraction = + BaseOnlyInitialFactAbstraction(this) + + override fun methodEdgesFinalApSet( + methodInitialStatement: CommonInst, + maxInstIdx: Int, + languageManager: LanguageManager, + ): MethodEdgesFinalApSet = + MethodEdgesFinalBaseOnlyApSet(methodInitialStatement, maxInstIdx, languageManager, this) + + override fun methodEdgesInitialToFinalApSet( + methodInitialStatement: CommonInst, + maxInstIdx: Int, + languageManager: LanguageManager, + ): MethodEdgesInitialToFinalApSet = + MethodEdgesInitialToFinalBaseOnlyApSet(methodInitialStatement, maxInstIdx, languageManager, this) + + override fun methodEdgesNDInitialToFinalApSet( + methodInitialStatement: CommonInst, + maxInstIdx: Int, + languageManager: LanguageManager, + ): MethodEdgesNDInitialToFinalApSet = + MethodEdgesNDInitialToFinalBaseOnlyApSet(methodInitialStatement, languageManager, maxInstIdx, this) + + override fun accessPathSubscription(): MethodAccessPathSubscription = + MethodBaseOnlyAccessPathSubscription(this) + + override fun sideEffectRequirementApStorage(): SideEffectRequirementApStorage = + BaseOnlySideEffectRequirementApStorage() + + override fun methodFinalApSummariesStorage(methodInitialStatement: CommonInst): MethodFinalApSummariesStorage = + MethodFinalBaseOnlyApSummariesStorage(methodInitialStatement, this) + + override fun methodInitialToFinalApSummariesStorage(methodInitialStatement: CommonInst): MethodInitialToFinalApSummariesStorage = + MethodInitialToFinalBaseOnlyApSummariesStorage(methodInitialStatement, this) + + override fun methodNDInitialToFinalApSummariesStorage(methodInitialStatement: CommonInst): MethodNDInitialToFinalApSummariesStorage = + MethodNDInitialToFinalBaseOnlyApSummariesStorage(methodInitialStatement, this) + + override fun factSideEffectSummariesApStorage(methodInitialStatement: CommonInst): FactSideEffectSummariesApStorage = + FactSESummariesBaseOnlyStorage(methodInitialStatement, this) + + override fun finalFactList(): FinalFactList = BaseOnlyFinalFactList(this) + + override fun createSerializer(context: SummarySerializationContext): ApSerializer = + BaseOnlySerializer(this, context) + +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDelta.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDelta.kt new file mode 100644 index 000000000..dc9eb1bd4 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDelta.kt @@ -0,0 +1,90 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp + +sealed interface BaseOnlyFinalDelta : FinalFactAp.Delta + +data object BaseOnlyEmptyFinalDelta : BaseOnlyFinalDelta { + override val isEmpty: Boolean get() = true + override fun startsWithAccessor(accessor: Accessor): Boolean = false + override fun getStartAccessors(): Set = emptySet() + override fun getAllAccessors(): Set = emptySet() + override fun readAccessor(accessor: Accessor): FinalFactAp.Delta? = null + override fun isAbstract(): Boolean = true +} + +class BaseOnlyNodeFinalDelta( + val manager: BaseOnlyApManager, + val access: BaseOnlyAccess, +) : BaseOnlyFinalDelta { + override val isEmpty: Boolean get() = false + + override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor) + + override fun getStartAccessors(): Set = manager.startAccessors(access) + + override fun getAllAccessors(): Set = manager.allAccessors(access) + + override fun readAccessor(accessor: Accessor): FinalFactAp.Delta? = + manager.readAccess(access, accessor)?.let { BaseOnlyNodeFinalDelta(manager, it) } + + override fun isAbstract(): Boolean = access.isRootAbstract + + override fun equals(other: Any?): Boolean = + this === other || (other is BaseOnlyNodeFinalDelta && access == other.access) + + override fun hashCode(): Int = access.hashCode() + + override fun toString(): String = manager.renderAccess(access) +} + +sealed interface BaseOnlyInitialDelta : InitialFactAp.Delta + +data object BaseOnlyEmptyInitialDelta : BaseOnlyInitialDelta { + override val isEmpty: Boolean get() = true + override fun startsWithAccessor(accessor: Accessor): Boolean = false + override fun getStartAccessors(): Set = emptySet() + override fun getAllAccessors(): Set = emptySet() + override fun readAccessor(accessor: Accessor): InitialFactAp.Delta? = null + override fun isAbstract(): Boolean = true + override fun concat(other: InitialFactAp.Delta): InitialFactAp.Delta = other +} + +class BaseOnlyNodeInitialDelta( + val manager: BaseOnlyApManager, + val access: BaseOnlyAccess, +) : BaseOnlyInitialDelta { + override val isEmpty: Boolean get() = false + + override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor) + + override fun getStartAccessors(): Set = manager.startAccessors(access) + + override fun getAllAccessors(): Set = manager.allAccessors(access) + + override fun readAccessor(accessor: Accessor): InitialFactAp.Delta? = + manager.readAccess(access, accessor)?.let { BaseOnlyNodeInitialDelta(manager, it) } + + override fun isAbstract(): Boolean = access.isRootAbstract + + override fun concat(other: InitialFactAp.Delta): InitialFactAp.Delta = when (other) { + BaseOnlyEmptyInitialDelta -> this + is BaseOnlyNodeInitialDelta -> { + BaseOnlyNodeInitialDelta( + manager, + BaseOnlyAccessOps.append(access, other.access) + ?: error("static-first invariant violated: delta compose") + ) + } + else -> error("Unexpected delta: $other") + } + + override fun equals(other: Any?): Boolean = + this === other || (other is BaseOnlyNodeInitialDelta && access == other.access) + + override fun hashCode(): Int = access.hashCode() + + override fun toString(): String = manager.renderAccess(access) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusion.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusion.kt new file mode 100644 index 000000000..f4d693243 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusion.kt @@ -0,0 +1,18 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor + +fun slotOfIdx(idx: AccessorIdx): Int = when { + idx.isStaticAccessor() -> 0 + idx.isFieldAccessor() || idx == ELEMENT_ACCESSOR_IDX -> 1 + else -> 2 +} + +fun IntOpenHashSet.excludesIdx(idx: AccessorIdx): Boolean = + contains(idx) || (idx.isTypeInfoAccessor() && contains(TYPE_INFO_GROUP_ACCESSOR_IDX)) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusionSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusionSet.kt new file mode 100644 index 000000000..bdaf63b3f --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusionSet.kt @@ -0,0 +1,235 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentHashMapOf +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.PersistentAccessorSet + +internal fun BaseOnlyApManager.compactExclusions(exclusions: ExclusionSet): ExclusionSet = + when (exclusions) { + ExclusionSet.Empty, ExclusionSet.Universe -> exclusions + is ExclusionSet.Concrete -> { + val set = exclusions.set + if (set is BaseOnlyExclusionAccessorSet && set.manager === this) { + exclusions + } else { + ExclusionSet.Concrete(BaseOnlyExclusionAccessorSet.from(this, set)) + } + } + } + +internal class BaseOnlyExclusionAccessorSet private constructor( + val manager: BaseOnlyApManager, + private val chunks: PersistentMap, + override val size: Int, + private val cachedHash: Int, +) : AbstractSet(), PersistentAccessorSet { + override fun contains(element: Accessor): Boolean = + containsIndex(manager.interner.index(element)) + + fun containsIndex(index: Int): Boolean { + val mask = chunks[index.chunkIndex()] ?: return false + return mask and index.chunkBit() != 0L + } + + fun forEachIndex(consume: (Int) -> Unit) { + chunks.forEach { (chunkIndex, bits) -> + var remaining = bits + while (remaining != 0L) { + val bit = remaining.countTrailingZeroBits() + consume((chunkIndex shl CHUNK_BITS) + bit) + remaining = remaining and (remaining - 1) + } + } + } + + fun union(other: BaseOnlyExclusionAccessorSet): BaseOnlyExclusionAccessorSet { + require(other.manager === manager) + return unionWithAdded(other)?.union ?: this + } + + fun unionIfChanged(other: BaseOnlyExclusionAccessorSet): BaseOnlyExclusionAccessorSet? { + require(other.manager === manager) + return unionWithAdded(other)?.union + } + + /** + * Adds [other] and returns both the union and the elements that [other] added. + * The unchanged case performs no allocation, which is important for repeated + * side-effect requirements. + */ + fun unionWithAdded(other: BaseOnlyExclusionAccessorSet): UnionWithAdded? { + require(other.manager === manager) + if (other.isEmpty()) return null + + var unionChunks = chunks + var addedChunks = persistentHashMapOf() + var addedSize = 0 + var addedHash = 0 + other.chunks.forEach { (chunkIndex, otherBits) -> + val currentBits = chunks[chunkIndex] ?: 0L + val newBits = otherBits and currentBits.inv() + if (newBits == 0L) return@forEach + + unionChunks = unionChunks.put(chunkIndex, currentBits or newBits) + addedChunks = addedChunks.put(chunkIndex, newBits) + var remaining = newBits + while (remaining != 0L) { + val bit = remaining.countTrailingZeroBits() + addedSize++ + addedHash += accessorHash((chunkIndex shl CHUNK_BITS) + bit) + remaining = remaining and (remaining - 1) + } + } + if (addedSize == 0) return null + + return UnionWithAdded( + union = BaseOnlyExclusionAccessorSet(manager, unionChunks, size + addedSize, cachedHash + addedHash), + added = BaseOnlyExclusionAccessorSet(manager, addedChunks, addedSize, addedHash), + ) + } + + override fun iterator(): Iterator = object : Iterator { + private val chunkIterator = chunks.entries.sortedBy { it.key }.iterator() + private var chunkIndex = 0 + private var remaining = 0L + + init { + advanceChunk() + } + + override fun hasNext(): Boolean = remaining != 0L + + override fun next(): Accessor { + if (!hasNext()) throw NoSuchElementException() + val bit = remaining.countTrailingZeroBits() + val index = (chunkIndex shl CHUNK_BITS) + bit + remaining = remaining and (remaining - 1) + if (remaining == 0L) advanceChunk() + return manager.interner.accessor(index) + ?: error("Accessor not found") + } + + private fun advanceChunk() { + if (!chunkIterator.hasNext()) return + val entry = chunkIterator.next() + chunkIndex = entry.key + remaining = entry.value + } + } + + override fun hashCode(): Int = cachedHash + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is BaseOnlyExclusionAccessorSet) { + return manager === other.manager && + size == other.size && cachedHash == other.cachedHash && chunks == other.chunks + } + return super.equals(other) + } + + override fun addPersistent(accessor: Accessor): PersistentAccessorSet { + val idx = manager.interner.index(accessor) + if (containsIndex(idx)) return this + val chunkIndex = idx.chunkIndex() + val result = chunks.put(chunkIndex, (chunks[chunkIndex] ?: 0L) or idx.chunkBit()) + return BaseOnlyExclusionAccessorSet(manager, result, size + 1, cachedHash + accessor.hashCode()) + } + + override fun addAllPersistent(accessors: Set): PersistentAccessorSet = + combine(accessors, SetOperation.Union) + + override fun retainAllPersistent(accessors: Set): PersistentAccessorSet = + combine(accessors, SetOperation.Intersection) + + override fun removePersistent(accessor: Accessor): PersistentAccessorSet { + val idx = manager.interner.index(accessor) + val chunkIndex = idx.chunkIndex() + val currentBits = chunks[chunkIndex] ?: return this + val bit = idx.chunkBit() + if (currentBits and bit == 0L) return this + if (size == 1) return empty(manager) + + val newBits = currentBits and bit.inv() + val result = if (newBits == 0L) chunks.remove(chunkIndex) else chunks.put(chunkIndex, newBits) + return BaseOnlyExclusionAccessorSet(manager, result, size - 1, cachedHash - accessor.hashCode()) + } + + override fun removeAllPersistent(accessors: Set): PersistentAccessorSet = + combine(accessors, SetOperation.Difference) + + private fun combine( + accessors: Set, + operation: SetOperation, + ): BaseOnlyExclusionAccessorSet { + val other = from(manager, accessors) + return when (operation) { + SetOperation.Union -> union(other) + SetOperation.Intersection -> filterIndices { other.containsIndex(it) } + SetOperation.Difference -> filterIndices { !other.containsIndex(it) } + } + } + + private inline fun filterIndices(crossinline keep: (Int) -> Boolean): BaseOnlyExclusionAccessorSet { + var resultChunks = persistentHashMapOf() + var resultSize = 0 + var resultHash = 0 + forEachIndex { index -> + if (!keep(index)) return@forEachIndex + val chunkIndex = index.chunkIndex() + resultChunks = resultChunks.put(chunkIndex, (resultChunks[chunkIndex] ?: 0L) or index.chunkBit()) + resultSize++ + resultHash += accessorHash(index) + } + return when (resultSize) { + size -> this + 0 -> empty(manager) + else -> BaseOnlyExclusionAccessorSet(manager, resultChunks, resultSize, resultHash) + } + } + + private fun accessorHash(index: Int): Int = + manager.interner.accessor(index)?.hashCode() ?: error("Accessor not found: $index") + + private enum class SetOperation { + Union, + Intersection, + Difference, + } + + companion object { + fun from(manager: BaseOnlyApManager, accessors: Set): BaseOnlyExclusionAccessorSet { + if (accessors is BaseOnlyExclusionAccessorSet && accessors.manager === manager) return accessors + + var chunks = persistentHashMapOf() + var size = 0 + var hash = 0 + accessors.forEach { accessor -> + val index = manager.interner.index(accessor) + val chunkIndex = index.chunkIndex() + val bit = index.chunkBit() + val currentBits = chunks[chunkIndex] ?: 0L + if (currentBits and bit != 0L) return@forEach + chunks = chunks.put(chunkIndex, currentBits or bit) + size++ + hash += accessor.hashCode() + } + return BaseOnlyExclusionAccessorSet(manager, chunks, size, hash) + } + + fun empty(manager: BaseOnlyApManager): BaseOnlyExclusionAccessorSet = + BaseOnlyExclusionAccessorSet(manager, persistentHashMapOf(), 0, 0) + + private const val CHUNK_BITS = 6 + + private fun Int.chunkIndex(): Int = this ushr CHUNK_BITS + private fun Int.chunkBit(): Long = 1L shl (this and 63) + } + + data class UnionWithAdded( + val union: BaseOnlyExclusionAccessorSet, + val added: BaseOnlyExclusionAccessorSet, + ) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FFieldGeneralization.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FFieldGeneralization.kt new file mode 100644 index 000000000..0f454bcb8 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FFieldGeneralization.kt @@ -0,0 +1,184 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FieldAccessor + +internal const val MAX_FIELD_ENUMERATION_EDGES = 8 + +internal data class BaseOnlySummaryEdgeAccessKey( + val initial: BaseOnlyAccess, + val final: BaseOnlyAccess, +) + +internal data class BaseOnlyFieldErasureGroup( + val initial: BaseOnlyAccess, + val final: BaseOnlyAccess, +) + +internal data class BaseOnlyFieldGeneralizationResult( + val summaries: List, + val newlyGeneralized: Set, +) + +internal data class BaseOnlyFieldGeneralizationUpdate( + val representative: BaseOnlySummaryEdge, + val absorbedMembers: Set, + val newlyGeneralized: Boolean, +) + +/** + * Writer-owned widening state for one initial-base/final-base storage scope. + */ +internal class BaseOnlyF2FFieldGeneralizer( + private val maxEnumeratedEdges: Int = MAX_FIELD_ENUMERATION_EDGES, + private val mergeExclusions: (List) -> ExclusionSet = { exclusions -> + exclusions.reduce(ExclusionSet::union) + }, +) { + private val generalizedGroups = linkedSetOf() + private val exclusionsByGroup = linkedMapOf() + private val membersByGroup = linkedMapOf< + BaseOnlyFieldErasureGroup, + LinkedHashMap, + >() + + fun groupOf(initial: BaseOnlyAccess, final: BaseOnlyAccess): BaseOnlyFieldErasureGroup? { + val erasedInitial = initial.eraseFieldForSummaryGeneralization() ?: return null + val erasedFinal = final.eraseFieldForSummaryGeneralization() ?: return null + return BaseOnlyFieldErasureGroup(erasedInitial, erasedFinal) + } + + fun isGeneralized(initial: BaseOnlyAccess, final: BaseOnlyAccess): Boolean = + groupOf(initial, final) in generalizedGroups + + /** + * Incrementally observes one canonical edge. Until the group crosses its budget the edge + * remains exact. Afterwards each new member only updates the already materialized + * representative. + */ + fun observeCanonicalEdge(edge: BaseOnlySummaryEdge): BaseOnlyFieldGeneralizationUpdate? { + val group = groupOf(edge.initial, edge.final) ?: return null + val currentRepresentativeExclusion = exclusionsByGroup[group] + if (group in generalizedGroups) { + val mergedExclusion = mergeExclusions(listOf(currentRepresentativeExclusion!!, edge.exclusion)) + if (mergedExclusion == currentRepresentativeExclusion) return null + exclusionsByGroup[group] = mergedExclusion + return BaseOnlyFieldGeneralizationUpdate( + representative = createRepresentative(group), + absorbedMembers = emptySet(), + newlyGeneralized = false, + ) + } + + val members = membersByGroup.getOrPut(group) { linkedMapOf() } + members[edge.accessKey] = edge.exclusion + if (members.size <= maxEnumeratedEdges) return null + + exclusionsByGroup[group] = mergeExclusions(members.values.toList()) + generalizedGroups += group + membersByGroup.remove(group) + return BaseOnlyFieldGeneralizationUpdate( + representative = createRepresentative(group), + absorbedMembers = members.keys.toSet(), + newlyGeneralized = true, + ) + } + + fun removeCanonicalEdge(edge: BaseOnlySummaryEdge) { + val group = groupOf(edge.initial, edge.final) ?: return + if (group in generalizedGroups) return + val members = membersByGroup[group] ?: return + members.remove(edge.accessKey) + if (members.isEmpty()) membersByGroup.remove(group) + } + + fun rewrite(summaries: List): BaseOnlyFieldGeneralizationResult { + val members = summaries.groupByTo(linkedMapOf()) { edge -> + groupOf(edge.initial, edge.final) + } + + val newlyGeneralized = linkedSetOf() + members.forEach { (group, edges) -> + if (group == null) return@forEach + + val observedExclusion = mergeExclusions(edges.map(BaseOnlySummaryEdge::exclusion)) + exclusionsByGroup[group] = if (group in generalizedGroups) { + mergeExclusions(listOf(exclusionsByGroup.getValue(group), observedExclusion)) + } else { + observedExclusion + } + + if (group !in generalizedGroups && edges.size > maxEnumeratedEdges) { + generalizedGroups += group + newlyGeneralized += group + } + } + + if (generalizedGroups.isEmpty()) { + return BaseOnlyFieldGeneralizationResult(summaries, emptySet()) + } + + val rewritten = summaries.filterTo(arrayListOf()) { edge -> + groupOf(edge.initial, edge.final) !in generalizedGroups + } + generalizedGroups.forEach { group -> + rewritten += createRepresentative(group) + } + rewritten.sortWith(BASE_ONLY_SUMMARY_EDGE_ORDER) + + return BaseOnlyFieldGeneralizationResult(rewritten, newlyGeneralized) + } + + fun representative(group: BaseOnlyFieldErasureGroup): BaseOnlySummaryEdge = + createRepresentative(group) + + private fun createRepresentative(group: BaseOnlyFieldErasureGroup): BaseOnlySummaryEdge = + BaseOnlySummaryEdge(group.initial, group.final, exclusionsByGroup.getValue(group)) +} + +internal val BaseOnlySummaryEdge.accessKey: BaseOnlySummaryEdgeAccessKey + get() = BaseOnlySummaryEdgeAccessKey(initial, final) + +internal fun intersectSummaryFieldGeneralizationExclusions( + exclusions: List, +): ExclusionSet = exclusions + .map(ExclusionSet::suffixExclusions) + .reduce(ExclusionSet::intersect) + +private fun ExclusionSet.suffixExclusions(): ExclusionSet = when (this) { + ExclusionSet.Empty, + ExclusionSet.Universe, + -> this + + is ExclusionSet.Concrete -> set.fold(ExclusionSet.Empty as ExclusionSet) { suffix, accessor -> + when (accessor) { + AnyAccessor, + ElementAccessor, + is ClassStaticAccessor, + is FieldAccessor, + -> suffix + + else -> suffix.add(accessor) + } + } +} + +internal fun BaseOnlyAccess.eraseFieldForSummaryGeneralization(): BaseOnlyAccess? { + if (staticIdx != NO_ACCESSOR || valueAccessorState != BaseOnlyValueAccessorState.Normal) return null + + val eligible = when { + fieldIdx == ABSTRACT_MARK && suffixIdx == NO_ACCESSOR -> true + fieldIdx.isStructuralIdx() && suffixIdx == ABSTRACT_MARK -> true + fieldIdx == NO_ACCESSOR && suffixIdx == ABSTRACT_MARK -> true + else -> false + } + return ABSTRACT_EMPTY_ACCESS.takeIf { eligible } +} + +internal val BASE_ONLY_SUMMARY_EDGE_ORDER = compareBy( + { it.initial }, + { it.final }, +) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactAp.kt new file mode 100644 index 000000000..4f70eebb1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactAp.kt @@ -0,0 +1,225 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor + +class BaseOnlyFinalFactAp( + val manager: BaseOnlyApManager, + override val base: AccessPathBase, + val access: BaseOnlyAccess, + exclusions: ExclusionSet, +) : FinalFactAp { + override val exclusions: ExclusionSet = manager.compactExclusions(exclusions) + + init { + BaseOnlyAccessOps.requireCanonical(access, allowTransientCollapsed = true) + } + + override val size: Int get() = access.size + override val depth: Int get() = size + + override fun isAbstract(): Boolean = access.isRootAbstract + + override fun rebase(newBase: AccessPathBase): FinalFactAp = + BaseOnlyFinalFactAp(manager, newBase, BaseOnlyAccessOps.restoreAbstraction(access), exclusions) + + override fun exclude(accessor: Accessor): FinalFactAp = + BaseOnlyFinalFactAp(manager, base, access, exclusions.add(accessor)) + + override fun replaceExclusions(exclusions: ExclusionSet): FinalFactAp = + BaseOnlyFinalFactAp(manager, base, access, exclusions) + + private fun rewrap(newAccess: BaseOnlyAccess): BaseOnlyFinalFactAp = + BaseOnlyFinalFactAp(manager, base, newAccess, exclusions) + + override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor) + + override fun getStartAccessors(): Set = manager.startAccessors(access) + + override fun getAllAccessors(): Set = manager.allAccessors(access) + + override fun readAccessor(accessor: Accessor): FinalFactAp? = manager.readAccess(access, accessor)?.let(::rewrap) + + override fun prependAccessor(accessor: Accessor): FinalFactAp = + rewrap(BaseOnlyAccessOps.prepend(access, manager.interner.index(accessor), manager.fieldSensitive)) + + override fun clearAccessor(accessor: Accessor): FinalFactAp? = + BaseOnlyAccessOps.clear(access, manager.interner.index(accessor))?.let(::rewrap) + + override fun removeAbstraction(): FinalFactAp? = + BaseOnlyAccessOps.collapse(access).takeIf { !it.isEmpty }?.let(::rewrap) + + override fun abstractOnly(): FinalFactAp { + val abstractAccess = access.withBaseOnlyAccessUnpacked { staticIdx, fieldIdx, _ -> + when { + staticIdx == ABSTRACT_MARK -> packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR) + fieldIdx == ABSTRACT_MARK -> packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR) + else -> ABSTRACT_EMPTY_ACCESS + } + } + return rewrap(abstractAccess) + } + + override fun filterFact(filter: FactTypeChecker.FactApFilter): FinalFactAp? = + filterAccess(filter)?.let { filtered -> if (filtered == access) this else rewrap(filtered) } + + override fun filterFact(filter: FactTypeChecker.FactCompatibilityFilter): FinalFactAp? { + if (filter is FactTypeChecker.AlwaysCompatibleFilter) return this + if (!access.hasAp) return this + + // Tree checks only an edge whose child node has direct abstract acceptance. + // Ancestor nodes that merely contain an abstract descendant are not checked. + val predecessor = when (access.apSlot) { + 0 -> NO_ACCESSOR + 1 -> access.staticIdx + 2 -> if (access.fieldIdx >= 0) access.fieldIdx else access.staticIdx + else -> error("Canonical abstract fact has no abstraction slot: $access") + } + if (predecessor < 0) return this + val accessor = manager.interner.accessor(predecessor) + ?: error("Accessor not found: $predecessor") + return when (filter.check(accessor)) { + FactTypeChecker.CompatibilityFilterResult.Compatible -> this + FactTypeChecker.CompatibilityFilterResult.NotCompatible -> null + } + } + + private fun filterAccess( + filter: FactTypeChecker.FactApFilter, + candidate: BaseOnlyAccess = access, + ): BaseOnlyAccess? { + if (!candidate.hasSemanticMark) { + return candidate.takeIf { logicalPaths(candidate).any { path -> pathAccepted(filter, path) } } + } + val common = logicalPrefix(candidate) + val path = when (candidate.valueAccessorState) { + BaseOnlyValueAccessorState.Normal -> common + intArrayOf(candidate.suffixIdx, FINAL_ACCESSOR_IDX) + BaseOnlyValueAccessorState.Value -> { + val valueAccessor = + if (candidate.hasTypeInfoSuffix) TYPE_INFO_GROUP_ACCESSOR_IDX else VALUE_ACCESSOR_IDX + common + intArrayOf(valueAccessor, candidate.suffixIdx, FINAL_ACCESSOR_IDX) + } + } + return candidate.takeIf { pathAccepted(filter, path) } + } + + private fun pathAccepted(filter: FactTypeChecker.FactApFilter, path: IntArray): Boolean { + var current = filter + path.forEach { idx -> + val accessor = manager.interner.accessor(idx) ?: error("Accessor not found: $idx") + when (val result = current.check(accessor)) { + FactTypeChecker.FilterResult.Accept -> return true + FactTypeChecker.FilterResult.Reject -> return false + is FactTypeChecker.FilterResult.FilterNext -> current = result.filter + } + } + return true + } + + /** The single logical path represented by one compact access. */ + private fun logicalPaths(candidate: BaseOnlyAccess): List { + val common = logicalPrefix(candidate) + val suffix = candidate.suffixIdx + if (suffix < 0) return listOf(common) + if (suffix == FINAL_ACCESSOR_IDX) return listOf(common + FINAL_ACCESSOR_IDX) + if (candidate.hasTypeInfoSuffix) { + return listOf(terminalLogicalPath(candidate, common, TYPE_INFO_GROUP_ACCESSOR_IDX)) + } + if (suffix.isTaintMarkAccessor()) { + return listOf(terminalLogicalPath(candidate, common, VALUE_ACCESSOR_IDX)) + } + return listOf(common + intArrayOf(suffix, FINAL_ACCESSOR_IDX)) + } + + private fun logicalPrefix(candidate: BaseOnlyAccess): IntArray = buildList { + if (candidate.staticIdx >= 0) add(candidate.staticIdx) + if (candidate.fieldIdx >= 0) add(candidate.fieldIdx) + }.toIntArray() + + private fun terminalLogicalPath( + candidate: BaseOnlyAccess, + common: IntArray, + valueAccessor: Int, + ): IntArray = when (candidate.valueAccessorState) { + BaseOnlyValueAccessorState.Normal -> common + intArrayOf(candidate.suffixIdx, FINAL_ACCESSOR_IDX) + BaseOnlyValueAccessorState.Value -> + common + intArrayOf(valueAccessor, candidate.suffixIdx, FINAL_ACCESSOR_IDX) + } + + override fun contains(factAp: InitialFactAp): Boolean { + factAp as BaseOnlyInitialFactAp + if (base != factAp.base) return false + if (!BaseOnlyAccessOps.containsAccess(access, factAp.access)) return false + val residualHead = BaseOnlyAccessOps.firstAccessorAfterAbstraction(access, factAp.access) + ?: return true + val accessor = manager.interner.accessor(residualHead) ?: return true + return accessor !in exclusions + } + + override fun equalTo(factAp: InitialFactAp): Boolean { + factAp as BaseOnlyInitialFactAp + if (base != factAp.base) return false + return BaseOnlyAccessOps.equalToInitial(access, factAp.access) + } + + override fun delta(other: InitialFactAp): List { + other as BaseOnlyInitialFactAp + if (base != other.base) return emptyList() + val match = BaseOnlyAccessOps.matchPrefix(access, other.access) + val result = ArrayList(2) + if (match.emptyDelta) result.add(BaseOnlyEmptyFinalDelta) + if (match.hasSuffix) { + manager.applyExclusions(match.suffix, other.exclusions)?.let { suffix -> + result.add(BaseOnlyNodeFinalDelta(manager, suffix)) + } + } + return result + } + + override fun hasEmptyDelta(other: InitialFactAp): Boolean { + other as BaseOnlyInitialFactAp + return base == other.base && access == other.access + } + + override fun concat(typeChecker: FactTypeChecker, delta: FinalFactAp.Delta): FinalFactAp? { + return when (val d = delta as BaseOnlyFinalDelta) { + BaseOnlyEmptyFinalDelta -> this + is BaseOnlyNodeFinalDelta -> { + val filteredDelta = filterDelta(typeChecker, d.access) ?: return null + BaseOnlyAccessOps.appendFinal(access, filteredDelta)?.let(::rewrap) + } + } + } + + private fun filterDelta(typeChecker: FactTypeChecker, delta: BaseOnlyAccess): BaseOnlyAccess? { + val prefix = buildList { + access.forEachCoreIdx { idx -> + add(manager.interner.accessor(idx) ?: error("Accessor not found: $idx")) + } + } + return filterAccess(typeChecker.accessPathFilter(prefix), delta) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BaseOnlyFinalFactAp) return false + return base == other.base && access == other.access && exclusions == other.exclusions + } + + override fun hashCode(): Int { + var result = base.hashCode() + result = 31 * result + access.hashCode() + result = 31 * result + exclusions.hashCode() + return result + } + + override fun toString(): String = "$base${manager.renderAccess(access)}/$exclusions" +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactList.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactList.kt new file mode 100644 index 000000000..5e3491ad4 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactList.kt @@ -0,0 +1,29 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.longs.LongArrayList +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.common.CommonFinalFactList + +class BaseOnlyFinalFactList( + override val apManager: BaseOnlyApManager, +) : CommonFinalFactList(), BaseOnlyFinalApAccess { + override val storage: AccessStorage = LongAccessStorage() + + override fun add(fact: FinalFactAp) { + fact as BaseOnlyFinalFactAp + if (fact.access.isCollapsed) return + super.add(fact) + } + + private class LongAccessStorage : AccessStorage { + private val storage = LongArrayList() + + override fun add(fact: BaseOnlyAccess) { + storage.add(fact) + } + + override fun get(idx: Int): BaseOnlyAccess = storage.getLong(idx) + + override fun removeLast(): BaseOnlyAccess = storage.removeLong(storage.size - 1) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndex.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndex.kt new file mode 100644 index 000000000..4ebbd44fb --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndex.kt @@ -0,0 +1,199 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.util.ConcurrentReadSafeInt2ObjectMap +import org.opentaint.dataflow.util.forEachEntry +import org.opentaint.dataflow.util.getOrCreateNullable +import org.opentaint.dataflow.util.int2ObjectMap + +/** + * A single-writer/multiple-reader index over the three packed BaseOnly access slots. + * + * Most summary and subscription indexes contain only a handful of accesses. Keeping those entries + * in an immutable flat list avoids allocating three hash tables per index. Once an index grows past + * [SMALL_INDEX_LIMIT], the writer atomically publishes the slot hierarchy used for indexed lookup. + */ +internal class BaseOnlyInitialAccessIndex { + private data class Entry(val access: BaseOnlyAccess, val value: V) + + private sealed interface State { + class Small(val entries: List>) : State + class Indexed(val hierarchy: Hierarchy) : State + } + + private class FieldNode { + val fields: ConcurrentReadSafeInt2ObjectMap?> = int2ObjectMap() + } + + private class SuffixNode { + val suffixes: ConcurrentReadSafeInt2ObjectMap = int2ObjectMap() + } + + private class Hierarchy { + private val statics: ConcurrentReadSafeInt2ObjectMap?> = int2ObjectMap() + + fun getOrCreate(access: BaseOnlyAccess, create: () -> V): V { + val fieldNode = statics.getOrCreateNullable(access.staticIdx) { FieldNode() } + val suffixNode = fieldNode.fields.getOrCreateNullable(access.fieldIdx) { SuffixNode() } + return suffixNode.suffixes.getOrCreateNullable(access.rawSuffixSlot, create) + } + + fun get(access: BaseOnlyAccess): V? = + statics.get(access.staticIdx) + ?.fields?.get(access.fieldIdx) + ?.suffixes?.get(access.rawSuffixSlot) + + fun collectAll(consume: (BaseOnlyAccess, V) -> Unit) { + statics.forEachEntry { staticIdx, fieldNode -> + fieldNode?.collectAll(staticIdx, consume) + } + } + + fun collectCandidates(pattern: BaseOnlyAccess, consume: (BaseOnlyAccess, V) -> Unit) { + if (pattern.staticIdx == ABSTRACT_MARK) { + collectAll(consume) + return + } + + statics.get(ABSTRACT_MARK)?.collectAll(ABSTRACT_MARK, consume) + val fieldNode = statics.get(pattern.staticIdx) ?: return + if (pattern.fieldIdx == ABSTRACT_MARK) { + fieldNode.collectAll(pattern.staticIdx, consume) + return + } + + fieldNode.fields.get(ABSTRACT_MARK)?.collectAll(pattern.staticIdx, ABSTRACT_MARK, consume) + when (pattern.fieldIdx) { + NO_ACCESSOR -> fieldNode.fields.forEachEntry { fieldIdx, suffixNode -> + suffixNode?.collectCandidates(pattern.staticIdx, fieldIdx, pattern, consume) + } + + else -> { + fieldNode.fields.get(pattern.fieldIdx)?.collectCandidates( + pattern.staticIdx, + pattern.fieldIdx, + pattern, + consume, + ) + fieldNode.fields.get(NO_ACCESSOR)?.collectCandidates( + pattern.staticIdx, + NO_ACCESSOR, + pattern, + consume, + ) + } + } + } + + private fun FieldNode.collectAll(staticIdx: Int, consume: (BaseOnlyAccess, V) -> Unit) { + fields.forEachEntry { fieldIdx, suffixNode -> + suffixNode?.collectAll(staticIdx, fieldIdx, consume) + } + } + + private fun SuffixNode.collectCandidates( + staticIdx: Int, + fieldIdx: Int, + pattern: BaseOnlyAccess, + consume: (BaseOnlyAccess, V) -> Unit, + ) { + if (pattern.suffixIdx == ABSTRACT_MARK) { + collectAll(staticIdx, fieldIdx, consume) + return + } + + val abstractSuffix = rawBaseOnlySuffixSlot(ABSTRACT_MARK, BaseOnlyValueAccessorState.Normal) + suffixes.get(abstractSuffix)?.let { value -> + consume(packBaseOnlyAccessFromRawSuffix(staticIdx, fieldIdx, abstractSuffix), value) + } + + val states = + if (pattern.hasSemanticMark) BaseOnlyValueAccessorState.entries + else listOf(BaseOnlyValueAccessorState.Normal) + for (state in states) { + val rawSuffix = rawBaseOnlySuffixSlot(pattern.suffixIdx, state) + suffixes.get(rawSuffix)?.let { value -> + consume(packBaseOnlyAccessFromRawSuffix(staticIdx, fieldIdx, rawSuffix), value) + } + } + } + + private fun SuffixNode.collectAll( + staticIdx: Int, + fieldIdx: Int, + consume: (BaseOnlyAccess, V) -> Unit, + ) { + suffixes.forEachEntry { rawSuffix, value -> + value?.let { consume(packBaseOnlyAccessFromRawSuffix(staticIdx, fieldIdx, rawSuffix), it) } + } + } + } + + @Volatile + private var state: State = State.Small(emptyList()) + + fun getOrCreate(access: BaseOnlyAccess, create: () -> V): V { + return when (val current = state) { + is State.Indexed -> current.hierarchy.getOrCreate(access, create) + is State.Small -> { + current.entries.firstOrNull { it.access == access }?.value?.let { return it } + val value = create() + if (current.entries.size < SMALL_INDEX_LIMIT) { + state = State.Small(current.entries + Entry(access, value)) + } else { + val hierarchy = Hierarchy() + current.entries.forEach { entry -> + hierarchy.getOrCreate(entry.access) { entry.value } + } + hierarchy.getOrCreate(access) { value } + state = State.Indexed(hierarchy) + } + value + } + } + } + + fun get(access: BaseOnlyAccess): V? = when (val current = state) { + is State.Indexed -> current.hierarchy.get(access) + is State.Small -> current.entries.firstOrNull { it.access == access }?.value + } + + fun collectAll(consume: (BaseOnlyAccess, V) -> Unit) { + when (val current = state) { + is State.Indexed -> current.hierarchy.collectAll(consume) + is State.Small -> current.entries.forEach { consume(it.access, it.value) } + } + } + + fun collectCandidates(pattern: BaseOnlyAccess, consume: (BaseOnlyAccess, V) -> Unit) { + when (val current = state) { + is State.Indexed -> current.hierarchy.collectCandidates(pattern, consume) + is State.Small -> current.entries.forEach { entry -> + if (isCandidate(pattern, entry.access)) consume(entry.access, entry.value) + } + } + } + + private fun isCandidate(pattern: BaseOnlyAccess, candidate: BaseOnlyAccess): Boolean { + if (pattern.staticIdx == ABSTRACT_MARK || candidate.staticIdx == ABSTRACT_MARK) return true + if (pattern.staticIdx != candidate.staticIdx) return false + + if (pattern.fieldIdx == ABSTRACT_MARK || candidate.fieldIdx == ABSTRACT_MARK) return true + val fieldMatches = when (pattern.fieldIdx) { + NO_ACCESSOR -> true + else -> candidate.fieldIdx == pattern.fieldIdx || candidate.fieldIdx == NO_ACCESSOR + } + if (!fieldMatches) return false + + if (pattern.suffixIdx == ABSTRACT_MARK || candidate.suffixIdx == ABSTRACT_MARK) return true + if (pattern.suffixIdx != candidate.suffixIdx) return false + return pattern.hasSemanticMark || candidate.valueAccessorState == BaseOnlyValueAccessorState.Normal + } + + private companion object { + const val SMALL_INDEX_LIMIT = 32 + } +} + +/** Tree's filterContains is a symmetric applicability query, not directional containment. */ +internal fun baseOnlySummaryInitialMatches(pattern: BaseOnlyAccess, initial: BaseOnlyAccess): Boolean = + BaseOnlyAccessOps.mayOverlap(pattern, initial) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstraction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstraction.kt new file mode 100644 index 000000000..433ecab02 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstraction.kt @@ -0,0 +1,287 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import it.unimi.dsi.fastutil.ints.IntArrayList +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAbstraction +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor + +class BaseOnlyInitialFactAbstraction( + private val manager: BaseOnlyApManager, +) : InitialFactAbstraction { + private val perBase = Object2ObjectOpenHashMap() + + private inner class BaseState { + val added = LongOpenHashSet() + val emitted = LongOpenHashSet() + val knownExclusionsByPattern = Long2ObjectOpenHashMap() + val factsByExclusion = Int2ObjectOpenHashMap() + val blockedAtByFact = Long2LongOpenHashMap() + val concreteTypeBlockerByFact = Long2IntOpenHashMap().apply { defaultReturnValue(NO_ACCESSOR) } + + fun addExclusionsAndFindUnblockedAccessors( + pattern: BaseOnlyAccess, + exclusions: Set, + ): IntArrayList? { + val compactExclusions = BaseOnlyExclusionAccessorSet.from(manager, exclusions) + val knownExclusions = knownExclusionsByPattern[pattern] + + if (knownExclusions == null) { + if (compactExclusions.isEmpty()) return null + + knownExclusionsByPattern[pattern] = compactExclusions + return newlyExcludedBlockedAccessors(compactExclusions, previouslyExcluded = null) + } + + val union = knownExclusions.unionIfChanged(compactExclusions) ?: return null + + knownExclusionsByPattern[pattern] = union + return newlyExcludedBlockedAccessors(compactExclusions, knownExclusions) + } + + fun excludes(blockedAt: BaseOnlyAccess, accessor: AccessorIdx): Boolean { + val typeGroupMatches = accessor.isTypeInfoAccessor() + val iterator = knownExclusionsByPattern.long2ObjectEntrySet().fastIterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (!exclusionPatternCovers(entry.longKey, blockedAt)) continue + val exclusions = entry.value + if (exclusions.containsIndex(accessor)) return true + if (typeGroupMatches && exclusions.containsIndex(TYPE_INFO_GROUP_ACCESSOR_IDX)) return true + } + return false + } + + fun registerBlockedFact(access: BaseOnlyAccess, blockedAt: BaseOnlyAccess, accessor: AccessorIdx) { + check(!blockedAtByFact.containsKey(access)) + blockedAtByFact.put(access, blockedAt) + check(factsByExclusion.computeIfAbsent(accessor) { LongOpenHashSet() }.add(access)) + if (accessor.isTypeInfoAccessor() && accessor != TYPE_INFO_GROUP_ACCESSOR_IDX) { + check(concreteTypeBlockerByFact.put(access, accessor) == NO_ACCESSOR) + check( + factsByExclusion + .computeIfAbsent(TYPE_INFO_GROUP_ACCESSOR_IDX) { LongOpenHashSet() } + .add(access) + ) + } + } + + fun takeFactsUnblockedBy(accessor: AccessorIdx, pattern: BaseOnlyAccess): LongOpenHashSet? { + val candidates = factsByExclusion[accessor] ?: return null + val unblocked = LongOpenHashSet() + val candidateIterator = candidates.iterator() + while (candidateIterator.hasNext()) { + val access = candidateIterator.nextLong() + val blockedAt = blockedAtByFact.get(access) + if (!exclusionPatternCovers(pattern, blockedAt)) continue + + candidateIterator.remove() + unblocked.add(access) + blockedAtByFact.remove(access) + val concreteTypeBlocker = concreteTypeBlockerByFact.remove(access) + if (accessor == TYPE_INFO_GROUP_ACCESSOR_IDX && concreteTypeBlocker != NO_ACCESSOR) { + factsByExclusion[concreteTypeBlocker]?.remove(access) + } else if (concreteTypeBlocker != NO_ACCESSOR) { + factsByExclusion[TYPE_INFO_GROUP_ACCESSOR_IDX]?.remove(access) + } + } + if (candidates.isEmpty()) factsByExclusion.remove(accessor) + return unblocked.takeUnless { it.isEmpty() } + } + + private fun newlyExcludedBlockedAccessors( + exclusions: BaseOnlyExclusionAccessorSet, + previouslyExcluded: BaseOnlyExclusionAccessorSet?, + ): IntArrayList? { + var result: IntArrayList? = null + val iterator = factsByExclusion.keys.iterator() + while (iterator.hasNext()) { + val accessor = iterator.nextInt() + if (exclusions.containsIndex(accessor) && previouslyExcluded?.containsIndex(accessor) != true) { + val matches = result ?: IntArrayList().also { result = it } + matches.add(accessor) + } + } + return result + } + + private fun exclusionPatternCovers(pattern: BaseOnlyAccess, blockedAt: BaseOnlyAccess): Boolean = + pattern == ABSTRACT_EMPTY_ACCESS || BaseOnlyAccessOps.containsAccess(pattern, blockedAt) + } + + private data class Blocker(val accessor: AccessorIdx, val blockedAt: BaseOnlyAccess) + + override fun addAbstractedInitialFact( + factAp: FinalFactAp, + typeChecker: FactTypeChecker, + ): List> { + factAp as BaseOnlyFinalFactAp + val state = perBase.getOrPut(factAp.base) { BaseState() } + if (!state.added.add(factAp.access)) return emptyList() + + val out = ArrayList>() + abstractAndIndex(factAp.base, factAp.access, state, out) + return out + } + + override fun registerNewInitialFact( + factAp: InitialFactAp, + typeChecker: FactTypeChecker, + ): List> { + factAp as BaseOnlyInitialFactAp + val state = perBase.getOrPut(factAp.base) { BaseState() } + + val unblockedAccessors = when (val ex = factAp.exclusions) { + is ExclusionSet.Concrete -> state.addExclusionsAndFindUnblockedAccessors( + factAp.access, + ex.set, + ) + ExclusionSet.Empty -> null + ExclusionSet.Universe -> error("Unexpected universe exclusion") + } + if (unblockedAccessors == null) return emptyList() + + val out = ArrayList>() + val exclusionIterator = unblockedAccessors.iterator() + while (exclusionIterator.hasNext()) { + val accessor = exclusionIterator.nextInt() + val unblocked = state.takeFactsUnblockedBy(accessor, factAp.access) ?: continue + val unblockedIterator = unblocked.iterator() + while (unblockedIterator.hasNext()) { + abstractAndIndex(factAp.base, unblockedIterator.nextLong(), state, out) + } + } + return out + } + + private fun abstractAndIndex( + base: AccessPathBase, + added: BaseOnlyAccess, + state: BaseState, + out: MutableList>, + ) { + val blocker = abstractOneBranch(base, added, state, out) + if (blocker != null) state.registerBlockedFact(added, blocker.blockedAt, blocker.accessor) + } + + private fun abstractOneBranch( + base: AccessPathBase, + added: BaseOnlyAccess, + state: BaseState, + out: MutableList>, + ): Blocker? { + val prefix = ArrayList(3) + var stopped = false + val core = buildList { + if (added.staticIdx >= 0) add(added.staticIdx) + if (added.fieldIdx >= 0) add(added.fieldIdx) + if (added.hasSemanticMark && added.valueAccessorState == BaseOnlyValueAccessorState.Value) { + add(if (added.hasTypeInfoSuffix) TYPE_INFO_GROUP_ACCESSOR_IDX else VALUE_ACCESSOR_IDX) + } + if (added.suffixIdx >= 0 && added.suffixIdx != FINAL_ACCESSOR_IDX) add(added.suffixIdx) + } + var blocker: Blocker? = null + core.forEach { accessor -> + if (!stopped) { + val blockedAt = abstractAccess(prefix, slotOfIdx(accessor)) + emit( + base, prefix, slotOfIdx(accessor), isAbstract = true, exact = false, + valueAccessorState = BaseOnlyValueAccessorState.Normal, state, out, + ) + if (state.excludes(blockedAt, accessor)) { + prefix.add(accessor) + } else { + stopped = true + blocker = Blocker(accessor, blockedAt) + } + } + } + if (!stopped) { + if (added.hasAp) { + emit( + base, prefix, apSlot = added.apSlot, isAbstract = true, exact = false, + valueAccessorState = BaseOnlyValueAccessorState.Normal, state, out, + ) + } else { + emit( + base, prefix, apSlot = 2, isAbstract = false, exact = true, + valueAccessorState = added.valueAccessorState, state, out, + ) + } + } + return blocker + } + + private fun abstractAccess(prefix: List, apSlot: Int): BaseOnlyAccess { + var committedStatic = NO_ACCESSOR + var committedField = NO_ACCESSOR + for (idx in prefix) { + when { + idx.isStaticAccessor() -> committedStatic = idx + idx.isFieldAccessor() || idx == ELEMENT_ACCESSOR_IDX -> committedField = idx + } + } + return BaseOnlyAccessOps.abstractAt(committedStatic, committedField, apSlot) + } + + private fun emit( + base: AccessPathBase, + prefix: List, + apSlot: Int, + isAbstract: Boolean, + exact: Boolean, + valueAccessorState: BaseOnlyValueAccessorState, + state: BaseState, + out: MutableList>, + ) { + if (exact) { + val abstractAccess = abstractAccess(prefix, apSlot) + if (state.emitted.add(abstractAccess)) { + out.add( + BaseOnlyInitialFactAp(manager, base, abstractAccess, ExclusionSet.Empty) + to BaseOnlyFinalFactAp(manager, base, abstractAccess, ExclusionSet.Empty) + ) + } + var concreteAccess = BaseOnlyAccessOps.build( + (prefix + FINAL_ACCESSOR_IDX).toIntArray(), + isAbstract = false, + ) + if (concreteAccess.hasSemanticMark) concreteAccess = concreteAccess.withValueAccessorState(valueAccessorState) + if (state.emitted.add(concreteAccess)) { + out.add( + BaseOnlyInitialFactAp(manager, base, concreteAccess, ExclusionSet.Empty) + to BaseOnlyFinalFactAp(manager, base, concreteAccess, ExclusionSet.Empty) + ) + } + return + } + + val initialAccess: BaseOnlyAccess + val finalAccess: BaseOnlyAccess + val apAccess = abstractAccess(prefix, apSlot) + if (!state.emitted.add(apAccess)) return + initialAccess = apAccess + finalAccess = apAccess + + val initial = BaseOnlyInitialFactAp(manager, base, initialAccess, ExclusionSet.Empty) + val final = BaseOnlyFinalFactAp(manager, base, finalAccess, ExclusionSet.Empty) + out.add(initial to final) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAp.kt new file mode 100644 index 000000000..b4b27fd9d --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAp.kt @@ -0,0 +1,97 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp + +class BaseOnlyInitialFactAp( + val manager: BaseOnlyApManager, + override val base: AccessPathBase, + val access: BaseOnlyAccess, + exclusions: ExclusionSet, +) : InitialFactAp { + override val exclusions: ExclusionSet = manager.compactExclusions(exclusions) + + init { + BaseOnlyAccessOps.requireCanonical(access) + } + + override val size: Int get() = access.size + override val depth: Int get() = access.size + + override fun isAbstract(): Boolean = access.isRootAbstract + + override fun rebase(newBase: AccessPathBase): InitialFactAp = + BaseOnlyInitialFactAp(manager, newBase, access, exclusions) + + override fun exclude(accessor: Accessor): InitialFactAp = + BaseOnlyInitialFactAp(manager, base, access, exclusions.add(accessor)) + + override fun replaceExclusions(exclusions: ExclusionSet): InitialFactAp = + BaseOnlyInitialFactAp(manager, base, access, exclusions) + + private fun rewrap(newAccess: BaseOnlyAccess): BaseOnlyInitialFactAp = + BaseOnlyInitialFactAp(manager, base, newAccess, exclusions) + + override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor) + + override fun getStartAccessors(): Set = manager.startAccessors(access) + + override fun getAllAccessors(): Set = manager.allAccessors(access) + + override fun readAccessor(accessor: Accessor): InitialFactAp? = manager.readAccess(access, accessor)?.let(::rewrap) + + override fun prependAccessor(accessor: Accessor): InitialFactAp = + rewrap(BaseOnlyAccessOps.prepend(access, manager.interner.index(accessor), manager.fieldSensitive)) + + override fun clearAccessor(accessor: Accessor): InitialFactAp? = + BaseOnlyAccessOps.clear(access, manager.interner.index(accessor))?.let(::rewrap) + + override fun compatibilityFilter(typeChecker: FactTypeChecker): FactTypeChecker.FactCompatibilityFilter = + typeChecker.accessPathCompatibilityFilter( + buildList { access.forEachAccessorIdx { add(manager.interner.accessor(it) ?: error("Accessor not found: $it")) } } + ) + + override fun splitDelta(other: FinalFactAp): List> { + other as BaseOnlyFinalFactAp + if (base != other.base) return emptyList() + + return BaseOnlyAccessOps.splitDelta(access, other.access, manager, other.exclusions) + .map { (f, delta) -> rewrap(f) to delta } + } + + override fun concat(delta: InitialFactAp.Delta): InitialFactAp = + when (val d = delta as BaseOnlyInitialDelta) { + BaseOnlyEmptyInitialDelta -> this + is BaseOnlyNodeInitialDelta -> { + rewrap( + BaseOnlyAccessOps.append(access, d.access) + ?: error("static-first invariant violated: initial concat") + ) + } + } + + override fun contains(factAp: InitialFactAp): Boolean { + factAp as BaseOnlyInitialFactAp + if (base != factAp.base) return false + return BaseOnlyAccessOps.matchPrefix(access, factAp.access).emptyDelta + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BaseOnlyInitialFactAp) return false + return base == other.base && access == other.access && exclusions == other.exclusions + } + + override fun hashCode(): Int { + var result = base.hashCode() + result = 31 * result + access.hashCode() + result = 31 * result + exclusions.hashCode() + return result + } + + override fun toString(): String = "$base${manager.renderAccess(access)}/$exclusions" +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializer.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializer.kt new file mode 100644 index 000000000..0c632204d --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializer.kt @@ -0,0 +1,90 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.serialization.AccessPathBaseSerializer +import org.opentaint.dataflow.ap.ifds.serialization.ApSerializer +import org.opentaint.dataflow.ap.ifds.serialization.ExclusionSetSerializer +import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext +import java.io.DataInputStream +import java.io.DataOutputStream + +internal class BaseOnlySerializer( + private val manager: BaseOnlyApManager, + private val context: SummarySerializationContext, +) : ApSerializer { + private val exclusionSetSerializer = ExclusionSetSerializer(context) + + override fun DataOutputStream.writeFinalAp(ap: FinalFactAp) { + ap as BaseOnlyFinalFactAp + writeFact(ap.base, ap.exclusions, ap.access) + } + + override fun DataOutputStream.writeInitialAp(ap: InitialFactAp) { + ap as BaseOnlyInitialFactAp + writeFact(ap.base, ap.exclusions, ap.access) + } + + override fun DataInputStream.readFinalAp(): FinalFactAp { + val fact = readFact() + return BaseOnlyFinalFactAp(manager, fact.base, fact.access, fact.exclusions) + } + + override fun DataInputStream.readInitialAp(): InitialFactAp { + val fact = readFact() + return BaseOnlyInitialFactAp(manager, fact.base, fact.access, fact.exclusions) + } + + private fun DataOutputStream.writeFact(base: AccessPathBase, exclusions: ExclusionSet, access: BaseOnlyAccess) { + BaseOnlyAccessOps.requireCanonical(access) + with(AccessPathBaseSerializer) { writeAccessPathBase(base) } + with(exclusionSetSerializer) { writeExclusionSet(exclusions) } + writeSlot(access.staticIdx) + writeSlot(access.fieldIdx) + writeSlot(access.suffixIdx) + writeByte(access.valueAccessorState.encoded) + } + + private fun DataInputStream.readFact(): DeserializedFact { + val base = with(AccessPathBaseSerializer) { readAccessPathBase() } + val exclusions = with(exclusionSetSerializer) { readExclusionSet() } + val staticIdx = readSlot() + val fieldIdx = readSlot() + val suffixIdx = readSlot() + val valueAccessorState = BaseOnlyValueAccessorState.decode(readUnsignedByte()) + val access = BaseOnlyAccessOps.requireCanonical( + packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, valueAccessorState) + ) + return DeserializedFact(base, exclusions, access) + } + + private fun DataOutputStream.writeSlot(idx: AccessorIdx) { + when (idx) { + NO_ACCESSOR, ABSTRACT_MARK -> writeByte(idx) + else -> { + writeByte(ACCESSOR_SLOT) + val accessor = manager.interner.accessor(idx) ?: error("Accessor not found: $idx") + writeLong(context.getIdByAccessor(accessor)) + } + } + } + + private fun DataInputStream.readSlot(): AccessorIdx = when (val tag = readByte().toInt()) { + NO_ACCESSOR, ABSTRACT_MARK -> tag + ACCESSOR_SLOT -> manager.interner.index(context.getAccessorById(readLong())) + else -> error("Unexpected BaseOnly access slot tag: $tag") + } + + private class DeserializedFact( + val base: AccessPathBase, + val exclusions: ExclusionSet, + val access: BaseOnlyAccess, + ) + + private companion object { + const val ACCESSOR_SLOT = 0 + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementApStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementApStorage.kt new file mode 100644 index 000000000..21e8e9dc1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementApStorage.kt @@ -0,0 +1,124 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.SideEffectRequirementApStorage +import java.util.concurrent.ConcurrentHashMap + +class BaseOnlySideEffectRequirementApStorage : SideEffectRequirementApStorage { + private val based = ConcurrentHashMap() + + override fun add(requirements: List): List { + val modified = mutableListOf() + + for (requirement in requirements) { + requirement as BaseOnlyInitialFactAp + if (requirement.access.isCollapsed) continue + val storage = based.computeIfAbsent(requirement.base) { RequirementStorage() } + if (storage.mergeAdd(requirement) != null) modified += storage + } + + val result = mutableListOf() + modified.forEach { it.getAndResetDelta(result) } + return result + } + + + override fun filterTo(dst: MutableList, fact: FinalFactAp) { + fact as BaseOnlyFinalFactAp + val storage = based[fact.base] ?: return + storage.filterTo(dst, fact.access) + } + + override fun collectAllRequirementsTo(dst: MutableList) { + based.values.forEach { storage -> + storage.collectAllTo(dst) + } + } + + private class RequirementStorage { + private class RequirementNode(initial: BaseOnlyInitialFactAp) { + @Volatile + var requirement: BaseOnlyInitialFactAp = initial + } + + private val requirements = BaseOnlyInitialAccessIndex() + private val delta = Long2ObjectOpenHashMap() + + fun mergeAdd(requirement: BaseOnlyInitialFactAp): BaseOnlyInitialFactAp? { + var added = false + val node = requirements.getOrCreate(requirement.access) { + added = true + RequirementNode(requirement) + } + if (added) { + delta.put(requirement.access, requirement) + return requirement + } + + val previous = node.requirement + val update = previous.mergeWithAdded(requirement) ?: return null + val merged = update.merged + node.requirement = merged + + val addedRequirement = requirement.replaceExclusions(update.added) as BaseOnlyInitialFactAp + val previousDelta = delta[requirement.access] + val mergedDelta = checkNotNull(previousDelta.mergeAdd(addedRequirement)) + delta.put(requirement.access, mergedDelta) + return addedRequirement + } + + fun getAndResetDelta(dst: MutableList) { + dst.addAll(delta.values) + delta.clear() + } + + fun filterTo(dst: MutableList, fact: BaseOnlyAccess) { + requirements.collectCandidates(fact) { _, node -> + val requirement = node.requirement + if (baseOnlySummaryInitialMatches(fact, requirement.access)) { + dst.add(requirement) + } + } + } + + fun collectAllTo(dst: MutableList) { + requirements.collectAll { _, node -> dst.add(node.requirement) } + } + } +} + +private data class ExclusionMerge( + val merged: BaseOnlyInitialFactAp, + val added: ExclusionSet, +) + +private fun BaseOnlyInitialFactAp.mergeWithAdded(requirement: BaseOnlyInitialFactAp): ExclusionMerge? { + val previousExclusions = exclusions + val incomingExclusions = requirement.exclusions + if (incomingExclusions is ExclusionSet.Empty) return null + if (previousExclusions is ExclusionSet.Empty) { + return ExclusionMerge(requirement, incomingExclusions) + } + check(previousExclusions is ExclusionSet.Concrete && incomingExclusions is ExclusionSet.Concrete) + + val previousSet = previousExclusions.set as BaseOnlyExclusionAccessorSet + val incomingSet = incomingExclusions.set as BaseOnlyExclusionAccessorSet + val update = previousSet.unionWithAdded(incomingSet) ?: return null + val mergedExclusions = ExclusionSet.Concrete(update.union) + val addedExclusions = ExclusionSet.Concrete(update.added) + return ExclusionMerge( + BaseOnlyInitialFactAp(requirement.manager, requirement.base, requirement.access, mergedExclusions), + addedExclusions, + ) +} + +private fun BaseOnlyInitialFactAp?.mergeAdd(requirement: BaseOnlyInitialFactAp): BaseOnlyInitialFactAp? { + if (this == null) return requirement + val mergedExclusion = exclusions.union(requirement.exclusions) + if (mergedExclusion === exclusions) return null + return BaseOnlyInitialFactAp(requirement.manager, requirement.base, requirement.access, mergedExclusion) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTracker.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTracker.kt new file mode 100644 index 000000000..fb3eb45a1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTracker.kt @@ -0,0 +1,63 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp + +/** + * Tracks the exclusion information already applied for one side-effect requirement. + * + * Exclusions grow monotonically. The access paths identify the operation while the returned + * requirement contains only the exclusion delta that has not been applied for that operation. + */ +internal class BaseOnlySideEffectRequirementDeltaTracker { + private data class Key( + val currentBase: AccessPathBase, + val currentAccess: BaseOnlyAccess, + val requirementBase: AccessPathBase, + val requirementAccess: BaseOnlyAccess, + ) + + private val appliedExclusions = hashMapOf() + + fun add( + currentInitial: InitialFactAp, + requirement: InitialFactAp, + ): InitialFactAp? { + currentInitial as BaseOnlyInitialFactAp + requirement as BaseOnlyInitialFactAp + + val key = Key(currentInitial.base, currentInitial.access, requirement.base, requirement.access) + val previous = appliedExclusions[key] + if (previous == null) { + appliedExclusions[key] = requirement.exclusions + return requirement + } + + val incoming = requirement.exclusions + val update = when { + incoming is ExclusionSet.Empty || previous is ExclusionSet.Universe -> null + incoming is ExclusionSet.Universe -> ExclusionUpdate(incoming, incoming) + previous is ExclusionSet.Empty -> ExclusionUpdate(incoming, incoming) + else -> { + check(previous is ExclusionSet.Concrete && incoming is ExclusionSet.Concrete) + val previousSet = previous.set as BaseOnlyExclusionAccessorSet + val incomingSet = incoming.set as BaseOnlyExclusionAccessorSet + previousSet.unionWithAdded(incomingSet)?.let { + ExclusionUpdate( + merged = ExclusionSet.Concrete(it.union), + added = ExclusionSet.Concrete(it.added), + ) + } + } + } ?: return null + + appliedExclusions[key] = update.merged + return requirement.replaceExclusions(update.added) + } + + private data class ExclusionUpdate( + val merged: ExclusionSet, + val added: ExclusionSet, + ) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryEdgeOps.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryEdgeOps.kt new file mode 100644 index 000000000..6c9a9d3f0 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryEdgeOps.kt @@ -0,0 +1,140 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ExclusionSet + +internal data class BaseOnlySummaryEdge( + val initial: BaseOnlyAccess, + val final: BaseOnlyAccess, + val exclusion: ExclusionSet, +) + +/** + * Semantic operations on a single BaseOnly fact-to-fact summary edge. + * + * An edge is a correlated transformation: the residual consumed after [BaseOnlySummaryEdge.initial] + * must be grafted after [BaseOnlySummaryEdge.final]. When premises differ, independently comparing + * the two access paths is therefore not a valid subsumption test. When premises are identical, + * correlation is already fixed and directional conclusion coverage is sufficient. + */ +internal object BaseOnlySummaryEdgeOps { + fun canonicallyCovers( + manager: BaseOnlyApManager, + cover: BaseOnlySummaryEdge, + covered: BaseOnlySummaryEdge, + ): Boolean { + if (!subsumes(manager, cover, covered)) return false + if (!subsumes(manager, covered, cover)) return true + return BASE_ONLY_SUMMARY_EDGE_ORDER.compare(cover, covered) < 0 + } + + fun subsumes( + manager: BaseOnlyApManager, + general: BaseOnlySummaryEdge, + specific: BaseOnlySummaryEdge, + ): Boolean { + if (general.initial == specific.initial) { + val effectiveExclusion = specific.exclusion.union(general.exclusion) + return effectiveExclusion == specific.exclusion && + BaseOnlyAccessOps.covers(general.final, specific.final) + } + + val specificInitial = SummaryFact(specific.initial, specific.exclusion) + val specificFinal = SummaryFact(specific.final, specific.exclusion) + val application = applyEdge(manager, general, specificInitial) ?: return false + if (application.result != specificFinal) return false + + return reconstructInitials( + manager = manager, + edge = general, + final = specificFinal, + residual = application.residual, + ).any { it == specificInitial.access } + } + + private fun applyEdge( + manager: BaseOnlyApManager, + edge: BaseOnlySummaryEdge, + initial: SummaryFact, + ): SummaryApplication? { + val match = BaseOnlyAccessOps.matchPrefix(initial.access, edge.initial) + if (match.emptyDelta) { + return SummaryApplication( + residual = SummaryResidual.Empty, + result = SummaryFact(edge.final, initial.exclusion.union(edge.exclusion)), + ) + } + if (!match.hasSuffix) return null + + val residualAccess = retainResidual(manager, match.suffix, edge.exclusion) ?: return null + val resultAccess = BaseOnlyAccessOps.appendFinal(edge.final, residualAccess) ?: return null + return SummaryApplication( + residual = SummaryResidual.Access(residualAccess), + result = SummaryFact(resultAccess, initial.exclusion), + ) + } + + private fun reconstructInitials( + manager: BaseOnlyApManager, + edge: BaseOnlySummaryEdge, + final: SummaryFact, + residual: SummaryResidual, + ): Sequence { + return BaseOnlyAccessOps.splitDelta( + fact = final.access, + pattern = edge.final, + manager = manager, + exclusions = edge.exclusion, + ).asSequence().mapNotNull { (_, delta) -> + val reconstructedResidual = delta.toSummaryResidual(manager, edge.exclusion) ?: return@mapNotNull null + if (reconstructedResidual != residual) return@mapNotNull null + + when (reconstructedResidual) { + SummaryResidual.Empty -> edge.initial + is SummaryResidual.Access -> BaseOnlyAccessOps.append(edge.initial, reconstructedResidual.access) + } + } + } + + private fun BaseOnlyInitialDelta.toSummaryResidual( + manager: BaseOnlyApManager, + exclusions: ExclusionSet, + ): SummaryResidual? = when (this) { + BaseOnlyEmptyInitialDelta -> SummaryResidual.Empty + is BaseOnlyNodeInitialDelta -> + retainResidual(manager, access, exclusions)?.let(SummaryResidual::Access) + } + + /** + * Storage subsumption needs exact evidence that the residual branch survives. The ordinary + * BaseOnly exclusion operation may retain an excluded root terminal as a sound cover of its + * implicit-Any continuations; that widening must not be used to delete the explicit terminal + * edge itself. + */ + private fun retainResidual( + manager: BaseOnlyApManager, + residual: BaseOnlyAccess, + exclusions: ExclusionSet, + ): BaseOnlyAccess? = when (exclusions) { + ExclusionSet.Empty -> residual + ExclusionSet.Universe -> null + is ExclusionSet.Concrete -> { + val accessor = residual.headOrNull?.let(manager.interner::accessor) + residual.takeUnless { accessor != null && exclusions.contains(accessor) } + } + } + + private data class SummaryApplication( + val residual: SummaryResidual, + val result: SummaryFact, + ) + + private data class SummaryFact( + val access: BaseOnlyAccess, + val exclusion: ExclusionSet, + ) + + private sealed interface SummaryResidual { + data object Empty : SummaryResidual + data class Access(val access: BaseOnlyAccess) : SummaryResidual + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/FactSESummariesBaseOnlyStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/FactSESummariesBaseOnlyStorage.kt new file mode 100644 index 000000000..da1134454 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/FactSESummariesBaseOnlyStorage.kt @@ -0,0 +1,53 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.SideEffectKind +import org.opentaint.dataflow.ap.ifds.access.common.CommonFactSideEffectSummary +import org.opentaint.ir.api.common.cfg.CommonInst + +class FactSESummariesBaseOnlyStorage( + methodInitialInst: CommonInst, + override val apManager: BaseOnlyApManager, +) : CommonFactSideEffectSummary(methodInitialInst), + BaseOnlyInitialApAccess, BaseOnlyFinalApAccess { + override fun createStorage(): Storage = SEStorage(apManager) + + private class SEStorage(private val manager: BaseOnlyApManager) : Storage { + private val perInitial = BaseOnlyInitialAccessIndex() + + override fun add( + iap: BaseOnlyAccess, + se: Map, + added: MutableList>, + ) { + val storageNode = perInitial.getOrCreate(iap) { MergeStorage(manager, iap) } + for ((kind, exclusion) in se) { + storageNode.add(kind, exclusion)?.let { added += it } + } + } + + override fun collectSummariesTo( + dst: MutableList>, + initialFactPattern: BaseOnlyAccess?, + ) { + val collect: (BaseOnlyAccess, MergeStorage) -> Unit = { _, storage -> dst += storage.summaries() } + if (initialFactPattern == null) { + perInitial.collectAll(collect) + } else { + perInitial.collectCandidates(initialFactPattern) { initial, storage -> + if (baseOnlySummaryInitialMatches(initialFactPattern, initial)) collect(initial, storage) + } + } + } + } + + private class MergeStorage(private val manager: BaseOnlyApManager, private val initialAccess: BaseOnlyAccess) : + SideEffectExclusionMergingStorage() { + override fun createBuilder(): FactSEBuilder = Builder(manager).setInitialAp(initialAccess) + } + + private class Builder(override val apManager: BaseOnlyApManager) : + FactSEBuilder(), BaseOnlyInitialApAccess { + override fun nonNullIAP(iap: BaseOnlyAccess?): BaseOnlyAccess = iap ?: ABSTRACT_EMPTY_ACCESS + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodBaseOnlyAccessPathSubscription.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodBaseOnlyAccessPathSubscription.kt new file mode 100644 index 000000000..47c2e095e --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodBaseOnlyAccessPathSubscription.kt @@ -0,0 +1,156 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.common.CommonAPSub +import org.opentaint.dataflow.ap.ifds.access.common.CommonFactEdgeSubBuilder +import org.opentaint.dataflow.ap.ifds.access.common.CommonFactNDEdgeSubBuilder +import org.opentaint.dataflow.ap.ifds.access.common.CommonZeroEdgeSubBuilder +import org.opentaint.dataflow.ap.ifds.access.common.ndf2f.DefaultNDF2FSubStorageWithAp +import org.opentaint.ir.api.common.cfg.CommonInst +import java.util.BitSet + +class MethodBaseOnlyAccessPathSubscription( + override val apManager: BaseOnlyApManager, +) : CommonAPSub(), BaseOnlyInitialApAccess, BaseOnlyFinalApAccess { + + override fun createZ2FSubStorage(callerEp: CommonInst): Z2FSubStorage = + Z2FSub(apManager) + + override fun createF2FSubStorage(callerEp: CommonInst): F2FSubStorage = + F2FSub(apManager) + + override fun createNDF2FSubStorage(callerEp: CommonInst): NDF2FSubStorage = + NDSub(callerEp, apManager) + + private class Z2FSub(private val manager: BaseOnlyApManager) : + CommonAPSub.Z2FSubStorage { + private val edges = LongOpenHashSet() + private val edgeIndex = BaseOnlyInitialAccessIndex() + + override fun add(callerExitAp: BaseOnlyAccess): CommonZeroEdgeSubBuilder? { + if (!edges.add(callerExitAp)) return null + edgeIndex.getOrCreate(callerExitAp) { Unit } + return ZeroBuilder(manager).setNode(callerExitAp) + } + + override fun find(dst: MutableList>, summaryInitialFact: BaseOnlyAccess) { + edgeIndex.collectCandidates(summaryInitialFact) { exit, _ -> + val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact) + if (match.emptyDelta || match.hasSuffix) { + dst += ZeroBuilder(manager).setNode(exit) + } + } + } + } + + private class F2FSub(private val manager: BaseOnlyApManager) : + CommonAPSub.F2FSubStorage { + private val initialFactsByExit = + BaseOnlyInitialAccessIndex>() + + override fun add( + callerInitialAp: InitialFactAp, + callerExitAp: BaseOnlyAccess, + ): CommonFactEdgeSubBuilder? { + callerInitialAp as BaseOnlyInitialFactAp + val initialFacts = initialFactsByExit.getOrCreate(callerExitAp, ::hashSetOf) + if (!initialFacts.add(callerInitialAp)) return null + return FactBuilder(manager) + .setCallerNode(callerExitAp) + .setCallerInitialAp(callerInitialAp) + .setCallerExclusion(callerInitialAp.exclusions) + } + + override fun find( + dst: MutableList>, + summaryInitialFact: BaseOnlyAccess, + emptyDeltaRequired: Boolean, + ) { + initialFactsByExit.collectCandidates(summaryInitialFact) { exit, initialFacts -> + val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact) + if (!match.emptyDelta && !match.hasSuffix) return@collectCandidates + collectExit(dst, exit, initialFacts) + } + } + + private fun collectExit( + dst: MutableList>, + exit: BaseOnlyAccess, + initialFacts: Set, + ) { + initialFacts.forEach { initial -> + dst += FactBuilder(manager) + .setCallerNode(exit) + .setCallerInitialAp(initial) + .setCallerExclusion(initial.exclusions) + } + } + } + + private class NDSub(callerEp: CommonInst, private val manager: BaseOnlyApManager) : + DefaultNDF2FSubStorageWithAp(callerEp), BaseOnlyInitialApAccess { + override val apManager: BaseOnlyApManager get() = manager + + private val storageIndicesByExit = + BaseOnlyInitialAccessIndex>() + + override fun createBuilder(): CommonFactNDEdgeSubBuilder = NDBuilder(manager) + + override fun add( + callerInitial: Set, + callerExitAp: BaseOnlyAccess, + ): CommonFactNDEdgeSubBuilder? = super.add( + callerInitial.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) }, + callerExitAp, + ) + + override fun createStorage(idx: Int): Storage { + return FactStorage(idx) + } + + override fun relevantStorageIndices(summaryInitialFact: BaseOnlyAccess): BitSet { + val result = BitSet() + storageIndicesByExit.collectCandidates(summaryInitialFact) { exit, storageIndices -> + val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact) + if (match.emptyDelta || match.hasSuffix) { + storageIndices.forEach(result::set) + } + } + return result + } + + private inner class FactStorage( + private val storageIdx: Int, + ) : Storage { + private val edges = LongOpenHashSet() + + override fun add(element: BaseOnlyAccess): BaseOnlyAccess? { + if (!edges.add(element)) return null + storageIndicesByExit.getOrCreate(element, ::hashSetOf).add(storageIdx) + return element + } + + override fun collect(dst: MutableList) { + dst.addAll(edges) + } + + override fun collect(dst: MutableList, summaryInitialFact: BaseOnlyAccess) { + edges.forEach { exit -> + val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact) + if (match.emptyDelta || match.hasSuffix) dst.add(exit) + } + } + } + } + + private class ZeroBuilder(override val apManager: BaseOnlyApManager) : + CommonZeroEdgeSubBuilder(), BaseOnlyFinalApAccess + + private class FactBuilder(override val apManager: BaseOnlyApManager) : + CommonFactEdgeSubBuilder(), BaseOnlyFinalApAccess + + private class NDBuilder(override val apManager: BaseOnlyApManager) : + CommonFactNDEdgeSubBuilder(), BaseOnlyFinalApAccess +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesFinalBaseOnlyApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesFinalBaseOnlyApSet.kt new file mode 100644 index 000000000..13c40c6cf --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesFinalBaseOnlyApSet.kt @@ -0,0 +1,37 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import org.opentaint.dataflow.ap.ifds.LanguageManager +import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageIdx +import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageSize +import org.opentaint.dataflow.ap.ifds.access.common.CommonZ2FSet +import org.opentaint.ir.api.common.cfg.CommonInst + +class MethodEdgesFinalBaseOnlyApSet( + methodInitialStatement: CommonInst, + private val maxInstIdx: Int, + private val languageManager: LanguageManager, + override val apManager: BaseOnlyApManager, +) : CommonZ2FSet(methodInitialStatement), BaseOnlyFinalApAccess { + override fun createApStorage(): ApStorage = + ZeroInitialFactEdges(maxInstIdx, languageManager) + + private class ZeroInitialFactEdges( + maxInstIdx: Int, + private val languageManager: LanguageManager, + ) : ApStorage { + private val edges = arrayOfNulls(instructionStorageSize(maxInstIdx)) + + override fun addEdge(statement: CommonInst, accessPath: BaseOnlyAccess): BaseOnlyAccess? { + if (accessPath.isCollapsed) return null + val idx = instructionStorageIdx(statement, languageManager) + val set = edges[idx] ?: LongOpenHashSet().also { edges[idx] = it } + if (!set.add(accessPath)) return null + return accessPath + } + + override fun collectApAtStatement(statement: CommonInst, dst: MutableList) { + edges[instructionStorageIdx(statement, languageManager)]?.let { dst.addAll(it) } + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesInitialToFinalBaseOnlyApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesInitialToFinalBaseOnlyApSet.kt new file mode 100644 index 000000000..fa9fd83bd --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesInitialToFinalBaseOnlyApSet.kt @@ -0,0 +1,338 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.longs.LongArrayList +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.LanguageManager +import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageIdx +import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageSize +import org.opentaint.dataflow.ap.ifds.access.common.CommonF2FSet +import org.opentaint.ir.api.common.cfg.CommonInst + +class MethodEdgesInitialToFinalBaseOnlyApSet( + methodInitialStatement: CommonInst, + private val maxInstIdx: Int, + private val languageManager: LanguageManager, + override val apManager: BaseOnlyApManager, +) : CommonF2FSet(methodInitialStatement), + BaseOnlyInitialApAccess, BaseOnlyFinalApAccess { + + override fun mostAbstractPattern(base: AccessPathBase): BaseOnlyAccess = ABSTRACT_EMPTY_ACCESS + + override fun createApStorage(): ApStorage = Storage() + + private inner class Storage : ApStorage { + private val statements = arrayOfNulls(instructionStorageSize(maxInstIdx)) + + override fun add( + statement: CommonInst, + initial: BaseOnlyAccess, + final: AccessWithExclusion, + ): List> { + if (initial.isCollapsed || final.access.isCollapsed) return emptyList() + return statementState(statement, create = true)!!.add(initial, final) + } + + override fun filter( + dst: MutableList>>, + statement: CommonInst, + finalPattern: BaseOnlyAccess, + ) { + statementState(statement, create = false)?.collect(finalPattern) { initial, final -> + dst.add(initial to final) + } + + traceGeneralizationAt(statement)?.let { edge -> + val generalized = edge.initial to AccessWithExclusion(edge.final, edge.exclusion) + dst += generalized + } + } + + override fun filter( + dst: MutableList>, + statement: CommonInst, + initial: BaseOnlyAccess, + finalPattern: BaseOnlyAccess, + ) { + statementState(statement, create = false)?.collect(initial, finalPattern) { dst.add(it) } + + if (!apManager.traceResolutionModeEnabled()) return + + // Trace-time summary normalization exposes a field-abstract initial as a + // suffix-abstract alias. Resolve that view back to the primary intraprocedural + // key; the alias itself is never stored. + if (initial.apSlot == 2 && finalPattern.apSlot == 2) { + val primary = packBaseOnlyAccess(initial.staticIdx, ABSTRACT_MARK, NO_ACCESSOR) + statementState(statement, create = false)?.collect(primary, finalPattern) { dst.addDistinct(it) } + } + + traceGeneralizationAt(statement) + ?.takeIf { baseOnlySummaryInitialMatches(initial, it.initial) } + ?.let { dst.addDistinct(AccessWithExclusion(it.final, it.exclusion)) } + } + + private fun traceGeneralizationAt(statement: CommonInst): BaseOnlySummaryEdge? { + if (!apManager.traceResolutionModeEnabled() || !apManager.fieldGeneralizationEnabled) return null + + val exact = arrayListOf() + statementState(statement, create = false)?.collect(finalPattern = null) { initial, final -> + exact += BaseOnlySummaryEdge(initial, final.access, final.exclusion) + } + if (exact.isEmpty()) return null + + val generalizer = BaseOnlyF2FFieldGeneralizer(maxEnumeratedEdges = 0) + val result = generalizer.rewrite(exact) + val group = result.newlyGeneralized.singleOrNull() ?: return null + return generalizer.representative(group) + } + + private fun statementState(statement: CommonInst, create: Boolean): StatementState? { + val idx = instructionStorageIdx(statement, languageManager) + val current = statements[idx] + if (current != null || !create) return current + return StatementState().also { statements[idx] = it } + } + } + + private class StatementState { + private val initials = Long2ObjectOpenHashMap() + private val conclusions = BaseOnlyInitialAccessIndex() + + fun add( + initial: BaseOnlyAccess, + final: AccessWithExclusion, + ): List> { + val state = initials[initial] + if (state == null) { + initials.put(initial, InitialState(final)) + conclusion(final.access).add(initial) + return listOf(final) + } + + val update = state.add(final) + if (!update.changed) return emptyList() + update.removedFinals.forEach { removed -> + conclusions.get(removed)?.remove(initial) + } + if (update.finalAdded) conclusion(final.access).add(initial) + return update.delta + } + + fun collect( + finalPattern: BaseOnlyAccess?, + out: (BaseOnlyAccess, AccessWithExclusion) -> Unit, + ) { + val collectSupport: (BaseOnlyAccess, InitialSupport) -> Unit = collectSupport@{ final, support -> + if (support.isEmpty || + finalPattern != null && !baseOnlySummaryInitialMatches(finalPattern, final) + ) return@collectSupport + support.forEach { initial -> + val state = initials[initial] ?: error("Missing initial support") + out(initial, AccessWithExclusion(final, state.exclusion)) + } + } + if (finalPattern == null) { + conclusions.collectAll(collectSupport) + } else { + conclusions.collectCandidates(finalPattern, collectSupport) + } + } + + fun collect( + initial: BaseOnlyAccess, + finalPattern: BaseOnlyAccess, + out: (AccessWithExclusion) -> Unit, + ) { + initials[initial]?.collect(finalPattern, out) + } + + private fun conclusion(final: BaseOnlyAccess): InitialSupport = + conclusions.getOrCreate(final, ::InitialSupport) + } + + private class InitialState(first: AccessWithExclusion) { + private var firstFinal = first.access + private var multipleFinals: LongOpenHashSet? = null + var exclusion: ExclusionSet = first.exclusion + private set + + fun add(final: AccessWithExclusion): InitialUpdate { + val accessUpdate = addAccess(final.access) + val mergedExclusion = exclusion.union(final.exclusion) + val exclusionChanged = mergedExclusion != exclusion + if (!accessUpdate.changed && !exclusionChanged) return InitialUpdate.Unchanged + + exclusion = mergedExclusion + val delta = if (exclusionChanged) { + buildList { collect(finalPattern = null) { add(it) } } + } else { + listOf(AccessWithExclusion(final.access, exclusion)) + } + return InitialUpdate( + changed = true, + finalAdded = accessUpdate.changed, + removedFinals = accessUpdate.removed, + delta = delta, + ) + } + + fun collect( + finalPattern: BaseOnlyAccess?, + out: (AccessWithExclusion) -> Unit, + ) { + val finals = multipleFinals + if (finals == null) { + if (finalPattern == null || baseOnlySummaryInitialMatches(finalPattern, firstFinal)) { + out(AccessWithExclusion(firstFinal, exclusion)) + } + return + } + finals.forEach { access -> + if (finalPattern == null || baseOnlySummaryInitialMatches(finalPattern, access)) { + out(AccessWithExclusion(access, exclusion)) + } + } + } + + private fun addAccess(access: BaseOnlyAccess): AccessUpdate { + val finals = multipleFinals + if (finals != null) { + if (finals.containsCoverOf(access)) return AccessUpdate.Unchanged + + val removed = LongArrayList() + if (access.mayCoverDistinctAccess()) { + val covered = finals.iterator() + while (covered.hasNext()) { + val candidate = covered.nextLong() + if (BaseOnlyAccessOps.covers(access, candidate)) { + covered.remove() + removed.add(candidate) + } + } + } + + val added = finals.add(access) + if (!added) return AccessUpdate.Unchanged + if (finals.size == 1) { + firstFinal = access + multipleFinals = null + } + return AccessUpdate(true, removed) + } + if (BaseOnlyAccessOps.covers(firstFinal, access)) return AccessUpdate.Unchanged + if (BaseOnlyAccessOps.covers(access, firstFinal)) { + val removed = LongArrayList(1).also { it.add(firstFinal) } + firstFinal = access + return AccessUpdate(true, removed) + } + + multipleFinals = LongOpenHashSet(2).also { + it.add(firstFinal) + it.add(access) + } + return AccessUpdate(true, LongArrayList()) + } + } + + private class InitialSupport { + private var first: BaseOnlyAccess = NO_SUPPORT + private var multiple: LongOpenHashSet? = null + + val isEmpty: Boolean get() = first == NO_SUPPORT + + fun add(initial: BaseOnlyAccess) { + val supports = multiple + if (supports != null) { + supports.add(initial) + return + } + if (first == NO_SUPPORT) { + first = initial + } else if (first != initial) { + multiple = LongOpenHashSet(2).also { + it.add(first) + it.add(initial) + } + } + } + + fun remove(initial: BaseOnlyAccess) { + val supports = multiple + if (supports == null) { + if (first == initial) first = NO_SUPPORT + return + } + if (!supports.remove(initial)) return + if (supports.size == 1) { + first = supports.iterator().nextLong() + multiple = null + } + } + + fun forEach(action: (BaseOnlyAccess) -> Unit) { + multiple?.forEach(action) ?: first.takeUnless { it == NO_SUPPORT }?.let(action) + } + } + + private data class InitialUpdate( + val changed: Boolean, + val finalAdded: Boolean, + val removedFinals: LongArrayList, + val delta: List>, + ) { + companion object { + val Unchanged = InitialUpdate(false, false, LongArrayList(), emptyList()) + } + } + + private data class AccessUpdate( + val changed: Boolean, + val removed: LongArrayList, + ) { + companion object { + val Unchanged = AccessUpdate(false, LongArrayList()) + } + } + + private fun MutableList>.addDistinct( + value: AccessWithExclusion, + ) { + if (value !in this) add(value) + } + + private companion object { + const val NO_SUPPORT: BaseOnlyAccess = Long.MIN_VALUE + } +} + +private fun LongOpenHashSet.containsCoverOf(access: BaseOnlyAccess): Boolean { + if (contains(access)) return true + if (contains(packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR))) return true + if (access.staticIdx == ABSTRACT_MARK) return false + + if (contains(packBaseOnlyAccess(access.staticIdx, ABSTRACT_MARK, NO_ACCESSOR))) return true + if (access.fieldIdx == ABSTRACT_MARK) return false + + if (contains(packBaseOnlyAccess(access.staticIdx, access.fieldIdx, ABSTRACT_MARK))) return true + if (access.fieldIdx < 0) return false + + if (contains(packBaseOnlyAccess(access.staticIdx, NO_ACCESSOR, ABSTRACT_MARK))) return true + if (!access.hasSemanticMark) return false + + return contains( + packBaseOnlyAccess( + access.staticIdx, + NO_ACCESSOR, + access.suffixIdx, + access.valueAccessorState, + ) + ) +} + +private fun BaseOnlyAccess.mayCoverDistinctAccess(): Boolean = + staticIdx == ABSTRACT_MARK || + fieldIdx == ABSTRACT_MARK || + suffixIdx == ABSTRACT_MARK || + (fieldIdx == NO_ACCESSOR && hasSemanticMark) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesNDInitialToFinalBaseOnlyApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesNDInitialToFinalBaseOnlyApSet.kt new file mode 100644 index 000000000..12a0e5d92 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesNDInitialToFinalBaseOnlyApSet.kt @@ -0,0 +1,52 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.LanguageManager +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.common.CommonNDF2FSet +import org.opentaint.dataflow.ap.ifds.access.common.ndf2f.DefaultNDF2FSetStorage +import org.opentaint.ir.api.common.cfg.CommonInst + +class MethodEdgesNDInitialToFinalBaseOnlyApSet( + initialStatement: CommonInst, + languageManager: LanguageManager, + maxInstIdx: Int, + override val apManager: BaseOnlyApManager, +) : CommonNDF2FSet(initialStatement, languageManager, maxInstIdx), + BaseOnlyFinalApAccess, BaseOnlyInitialApAccess { + + override fun mostAbstractPattern(base: AccessPathBase): BaseOnlyAccess = ABSTRACT_EMPTY_ACCESS + + override fun add( + statement: CommonInst, + initial: Set, + finalAp: FinalFactAp, + ): Pair, FinalFactAp>? = + super.add( + statement, + initial.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) }, + finalAp, + ) + + override fun createApStorage(): ApStorage = + object : DefaultNDF2FSetStorage() { + override fun createStorage(): Storage = SetStorage(apManager) + } + + private class SetStorage(private val manager: BaseOnlyApManager) : DefaultNDF2FSetStorage.Storage { + private val set = LongOpenHashSet() + + override fun add(element: BaseOnlyAccess): BaseOnlyAccess? { + if (element.isCollapsed) return null + if (!set.add(element)) return null + return element + } + + override fun collect(dst: MutableList) { + dst.addAll(set) + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodFinalBaseOnlyApSummariesStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodFinalBaseOnlyApSummariesStorage.kt new file mode 100644 index 000000000..1532d2508 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodFinalBaseOnlyApSummariesStorage.kt @@ -0,0 +1,31 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.access.common.CommonZ2FSummary +import org.opentaint.dataflow.util.forEachLong +import org.opentaint.dataflow.util.longSet +import org.opentaint.ir.api.common.cfg.CommonInst + +class MethodFinalBaseOnlyApSummariesStorage( + methodInitialStatement: CommonInst, + override val apManager: BaseOnlyApManager, +) : CommonZ2FSummary(methodInitialStatement), BaseOnlyFinalApAccess { + override fun createStorage(): Storage = SummaryStorage(apManager) + + private class SummaryStorage(private val manager: BaseOnlyApManager) : Storage { + private val edges = longSet() + + override fun add(edges: List, added: MutableList>) { + for (edge in edges) { + if (edge.isCollapsed) continue + if (this.edges.add(edge)) added += Builder(manager).setNode(edge) + } + } + + override fun collectEdges(dst: MutableList>) { + edges.forEachLong { dst += Builder(manager).setNode(it) } + } + } + + private class Builder(override val apManager: BaseOnlyApManager) : + Z2FBBuilder(), BaseOnlyFinalApAccess +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodInitialToFinalBaseOnlyApSummariesStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodInitialToFinalBaseOnlyApSummariesStorage.kt new file mode 100644 index 000000000..962ef052a --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodInitialToFinalBaseOnlyApSummariesStorage.kt @@ -0,0 +1,351 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.InitialToFinalSummaryStorageStats +import org.opentaint.dataflow.ap.ifds.access.common.CommonF2FSummary +import org.opentaint.dataflow.util.ConcurrentReadSafeLong2ObjectMap +import org.opentaint.dataflow.util.forEachEntry +import org.opentaint.dataflow.util.long2ObjectMap +import org.opentaint.ir.api.common.cfg.CommonInst + +class MethodInitialToFinalBaseOnlyApSummariesStorage( + methodInitialStatement: CommonInst, + override val apManager: BaseOnlyApManager, +) : CommonF2FSummary(methodInitialStatement), + BaseOnlyInitialApAccess, BaseOnlyFinalApAccess { + override fun createStorage(): Storage = F2FStorage(apManager) + + private class F2FStorage( + private val manager: BaseOnlyApManager, + ) : Storage { + private val mergedExclusions = linkedMapOf() + private val rawKeysByFieldGroup = linkedMapOf< + BaseOnlyFieldErasureGroup, + MutableList, + >() + private val fieldGeneralizer = BaseOnlyF2FFieldGeneralizer( + mergeExclusions = ::intersectSummaryFieldGeneralizationExclusions, + ) + + private val summaries = CanonicalSummaryIndex() + + private class CanonicalSummaryIndex { + @Volatile + private var liveEdgeCount = 0L + + @Volatile + private var liveFinalFactSizeSum = 0L + + private class EdgeNode { + @Volatile + var exclusion: ExclusionSet? = null + } + + private class FinalIndex { + val finals: ConcurrentReadSafeLong2ObjectMap = long2ObjectMap() + val candidates = BaseOnlyInitialAccessIndex() + } + + private val initials = BaseOnlyInitialAccessIndex() + + fun put(edge: BaseOnlySummaryEdge): ExclusionSet? { + val finalIndex = initials.getOrCreate(edge.initial, ::FinalIndex) + val node = finalIndex.finals[edge.final] ?: EdgeNode().also { + finalIndex.finals.put(edge.final, it) + finalIndex.candidates.getOrCreate(edge.final) { it } + } + val previous = node.exclusion + node.exclusion = edge.exclusion + if (previous == null) { + liveEdgeCount++ + liveFinalFactSizeSum += edge.final.size + } + return previous + } + + fun remove(edge: BaseOnlySummaryEdge): Boolean { + val node = initials.get(edge.initial)?.finals?.get(edge.final) ?: return false + if (node.exclusion != edge.exclusion) return false + node.exclusion = null + liveEdgeCount-- + liveFinalFactSizeSum -= edge.final.size + return true + } + + fun get(key: BaseOnlySummaryEdgeAccessKey): ExclusionSet? = + initials.get(key.initial)?.finals?.get(key.final)?.exclusion + + fun stats(): InitialToFinalSummaryStorageStats = + InitialToFinalSummaryStorageStats(liveEdgeCount, liveFinalFactSizeSum) + + fun collectAll(consume: (BaseOnlySummaryEdge) -> Unit) { + initials.collectAll { initial, finals -> finals.collect(initial, consume) } + } + + fun collectCandidates(initial: BaseOnlyAccess, consume: (BaseOnlySummaryEdge) -> Unit) { + initials.collectCandidates(initial) { candidateInitial, finals -> + finals.collect(candidateInitial, consume) + } + } + + fun collectCandidates( + initial: BaseOnlyAccess, + final: BaseOnlyAccess, + consume: (BaseOnlySummaryEdge) -> Unit, + ) { + initials.collectCandidates(initial) { candidateInitial, finals -> + finals.collectCandidates(candidateInitial, final, consume) + } + } + + fun collectFinalCandidates( + final: BaseOnlyAccess, + consume: (BaseOnlySummaryEdge) -> Unit, + ) { + initials.collectAll { candidateInitial, finals -> + finals.collectCandidates(candidateInitial, final, consume) + } + } + + private fun FinalIndex.collect( + initial: BaseOnlyAccess, + consume: (BaseOnlySummaryEdge) -> Unit, + ) { + finals.forEachEntry { final, node -> + node.exclusion?.let { exclusion -> + consume(BaseOnlySummaryEdge(initial, final, exclusion)) + } + } + } + + private fun FinalIndex.collectCandidates( + initial: BaseOnlyAccess, + final: BaseOnlyAccess, + consume: (BaseOnlySummaryEdge) -> Unit, + ) { + candidates.collectCandidates(final) { candidateFinal, node -> + node.exclusion?.let { exclusion -> + consume(BaseOnlySummaryEdge(initial, candidateFinal, exclusion)) + } + } + } + } + + override fun add( + edges: List>, + added: MutableList>, + ) { + val newEdges = edges.filterNot { + it.initial.isCollapsed || + it.final.isCollapsed + } + if (newEdges.isEmpty()) return + + val pendingDelta = linkedMapOf() + val candidates = mergeExactEdges(newEdges) + candidates.sortedWith(BASE_ONLY_SUMMARY_EDGE_ORDER).forEach { candidate -> + if (manager.summaryStorageFieldGeneralizationEnabled && + fieldGeneralizer.isGeneralized(candidate.initial, candidate.final) + ) { + removeRawEdge(candidate.accessKey) + fieldGeneralizer.observeCanonicalEdge(candidate)?.let { update -> + insertCanonical(update.representative, pendingDelta, observeForGeneralization = false) + } + return@forEach + } + + insertCanonical(candidate, pendingDelta, observeForGeneralization = true) + } + + pendingDelta.values.forEach { edge -> + if (summaries.get(edge.accessKey) == edge.exclusion) added += edge.toBuilder() + } + } + + override fun collectSummariesTo( + dst: MutableList>, + initialFactPattern: BaseOnlyAccess?, + ) { + collectViews(initialFactPattern).forEach { (key, exclusion) -> + dst += BaseOnlySummaryEdge(key.initial, key.final, exclusion).toBuilder() + } + } + + override fun collectSummariesByFinalTo( + dst: MutableList>, + finalFactPattern: BaseOnlyAccess, + ) { + collectViews(initialFactPattern = null, finalFactPattern = finalFactPattern).forEach { (key, exclusion) -> + dst += BaseOnlySummaryEdge(key.initial, key.final, exclusion).toBuilder() + } + } + + override fun storageStats(): InitialToFinalSummaryStorageStats = summaries.stats() + + private fun mergeExactEdges( + edges: List>, + ): List { + val affectedKeys = linkedSetOf() + edges.forEach { edge -> + val key = BaseOnlySummaryEdgeAccessKey(edge.initial, edge.final) + affectedKeys += key + val previous = mergedExclusions[key] + mergedExclusions[key] = previous?.intersect(edge.exclusion) ?: edge.exclusion + if (manager.summaryStorageFieldGeneralizationEnabled && previous == null) { + fieldGeneralizer.groupOf(edge.initial, edge.final)?.let { group -> + rawKeysByFieldGroup.getOrPut(group, ::arrayListOf).add(key) + } + } + } + + return affectedKeys.map { key -> + BaseOnlySummaryEdge( + initial = key.initial, + final = key.final, + exclusion = mergedExclusions.getValue(key), + ) + } + } + + private fun insertCanonical( + candidate: BaseOnlySummaryEdge, + pendingDelta: MutableMap, + observeForGeneralization: Boolean, + ) { + val candidateKey = candidate.accessKey + val related = canonicalCandidates(candidate.initial, candidate.final) + for (existing in related) { + if (existing.accessKey == candidateKey) continue + if (BaseOnlySummaryEdgeOps.canonicallyCovers(manager, existing, candidate)) return + } + + putCanonical(candidate, pendingDelta) + related.forEach { existing -> + if (existing.accessKey != candidateKey && + BaseOnlySummaryEdgeOps.canonicallyCovers(manager, candidate, existing) + ) { + removeCanonical(existing, pendingDelta) + } + } + + if (!observeForGeneralization || !manager.summaryStorageFieldGeneralizationEnabled) return + val update = fieldGeneralizer.observeCanonicalEdge(candidate) ?: return + if (update.newlyGeneralized) purgeRawGroup(update.representative) + insertCanonical(update.representative, pendingDelta, observeForGeneralization = false) + update.absorbedMembers.forEach { member -> + currentEdge(member)?.let { removeCanonical(it, pendingDelta) } + } + if (update.newlyGeneralized) pendingDelta[update.representative.accessKey] = update.representative + } + + private fun putCanonical( + edge: BaseOnlySummaryEdge, + pendingDelta: MutableMap, + ) { + val key = edge.accessKey + val previous = summaries.put(edge) + if (previous != edge.exclusion) pendingDelta[key] = edge + } + + private fun removeCanonical( + edge: BaseOnlySummaryEdge, + pendingDelta: MutableMap, + ) { + val key = edge.accessKey + if (!summaries.remove(edge)) return + pendingDelta.remove(key) + fieldGeneralizer.removeCanonicalEdge(edge) + } + + private fun canonicalCandidates( + initial: BaseOnlyAccess, + final: BaseOnlyAccess, + ): List = buildList { + summaries.collectCandidates(initial, final) { add(it) } + } + + private fun currentEdge(key: BaseOnlySummaryEdgeAccessKey): BaseOnlySummaryEdge? = + summaries.get(key)?.let { exclusion -> BaseOnlySummaryEdge(key.initial, key.final, exclusion) } + + private fun removeRawEdge(key: BaseOnlySummaryEdgeAccessKey) { + mergedExclusions.remove(key) + } + + private fun purgeRawGroup(representative: BaseOnlySummaryEdge) { + val group = fieldGeneralizer.groupOf(representative.initial, representative.final) ?: return + rawKeysByFieldGroup.remove(group)?.forEach(mergedExclusions::remove) + } + + private fun collectViews( + initialFactPattern: BaseOnlyAccess?, + finalFactPattern: BaseOnlyAccess? = null, + ): Map { + val views = linkedMapOf() + fun collect(edge: BaseOnlySummaryEdge) { + views.addIfMatches( + initialFactPattern, + finalFactPattern, + edge.initial, + edge.final, + edge.exclusion, + ) + + if (manager.traceResolutionModeEnabled()) { + val normalizedInitial = normalizeSummaryInitialAccess(edge.initial, edge.final) + if (normalizedInitial != edge.initial) { + views.addIfMatches( + initialFactPattern, + finalFactPattern, + normalizedInitial, + edge.final, + edge.exclusion, + ) + } + } + } + + if (finalFactPattern != null) { + summaries.collectFinalCandidates(finalFactPattern, ::collect) + return views + } + + if (initialFactPattern == null || manager.traceResolutionModeEnabled()) { + summaries.collectAll(::collect) + return views + } + + summaries.collectCandidates(initialFactPattern, ::collect) + return views + } + + private fun MutableMap.addIfMatches( + initialPattern: BaseOnlyAccess?, + finalPattern: BaseOnlyAccess?, + initial: BaseOnlyAccess, + final: BaseOnlyAccess, + exclusion: ExclusionSet, + ) { + if (initialPattern != null && !baseOnlySummaryInitialMatches(initialPattern, initial)) return + if (finalPattern != null && !baseOnlySummaryInitialMatches(finalPattern, final)) return + val key = BaseOnlySummaryEdgeAccessKey(initial, final) + this[key] = this[key]?.intersect(exclusion) ?: exclusion + } + + private fun BaseOnlySummaryEdge.toBuilder(): F2FBBuilder = + Builder(manager) + .setInitialAp(initial) + .setExitAp(final) + .setExclusion(exclusion) + + } + + private class Builder(override val apManager: BaseOnlyApManager) : + F2FBBuilder(), BaseOnlyInitialApAccess, BaseOnlyFinalApAccess { + override fun nonNullIAP(iap: BaseOnlyAccess?): BaseOnlyAccess = iap ?: ABSTRACT_EMPTY_ACCESS + } +} + +internal fun normalizeSummaryInitialAccess(initial: BaseOnlyAccess, final: BaseOnlyAccess): BaseOnlyAccess { + if (initial.apSlot != 1 || final.apSlot != 2) return initial + return packBaseOnlyAccess(initial.staticIdx, NO_ACCESSOR, ABSTRACT_MARK) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodNDInitialToFinalBaseOnlyApSummariesStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodNDInitialToFinalBaseOnlyApSummariesStorage.kt new file mode 100644 index 000000000..cf24d5df4 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodNDInitialToFinalBaseOnlyApSummariesStorage.kt @@ -0,0 +1,77 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import it.unimi.dsi.fastutil.longs.LongArrayList +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.common.CommonNDF2FSummary +import org.opentaint.dataflow.ap.ifds.access.common.ndf2f.DefaultNDF2FSummaryStorageWithAp +import org.opentaint.dataflow.util.forEachLong +import org.opentaint.dataflow.util.longSet +import org.opentaint.ir.api.common.cfg.CommonInst +import java.util.BitSet +import java.util.concurrent.ConcurrentHashMap + +class MethodNDInitialToFinalBaseOnlyApSummariesStorage( + methodEntryPoint: CommonInst, + override val apManager: BaseOnlyApManager, +) : CommonNDF2FSummary(methodEntryPoint), BaseOnlyFinalApAccess { + + private inner class Builder : NDF2FBBuilder(), BaseOnlyFinalApAccess { + override val apManager: BaseOnlyApManager + get() = this@MethodNDInitialToFinalBaseOnlyApSummariesStorage.apManager + } + + override fun createStorage(): Storage = object : + DefaultNDF2FSummaryStorageWithAp(methodEntryPoint), + BaseOnlyInitialApAccess, + BaseOnlyFinalApAccess { + override val apManager: BaseOnlyApManager + get() = this@MethodNDInitialToFinalBaseOnlyApSummariesStorage.apManager + + private val initialAccessIndices = + ConcurrentHashMap>() + + override fun initialApAdded(idx: Int, ap: InitialFactAp) { + val byAccess = initialAccessIndices.computeIfAbsent(ap.base) { BaseOnlyInitialAccessIndex() } + val indexed = byAccess.getOrCreate(getInitialAccess(ap)) { idx } + check(indexed == idx) { "Different ND initial facts have the same canonical BaseOnly access" } + } + + override fun relevantInitialAp(summaryInitialFactPattern: FinalFactAp): BitSet { + val pattern = getFinalAccess(summaryInitialFactPattern) + val result = BitSet() + initialAccessIndices[summaryInitialFactPattern.base]?.collectCandidates(pattern) { initial, idx -> + if (baseOnlySummaryInitialMatches(pattern, initial)) result.set(idx) + } + return result + } + + override fun createBuilder(): NDF2FBBuilder = Builder() + + override fun createStorage(idx: Int): Storage = FactStorage(idx) + + private inner class FactStorage( + override val storageIdx: Int, + ) : Storage { + private val edges = longSet() + private val edgesDelta = LongArrayList() + + override fun add(element: BaseOnlyAccess): Storage? { + if (element.isCollapsed) return null + if (!edges.add(element)) return null + edgesDelta.add(element) + return this + } + + override fun getAndResetDelta(delta: MutableList) { + delta.addAll(edgesDelta) + edgesDelta.clear() + } + + override fun collectTo(dst: MutableList) { + edges.forEachLong(dst::add) + } + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/MethodEdgesInitialToFinalCactusApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/MethodEdgesInitialToFinalCactusApSet.kt index 938838a3d..220df5f65 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/MethodEdgesInitialToFinalCactusApSet.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/MethodEdgesInitialToFinalCactusApSet.kt @@ -29,12 +29,12 @@ class MethodEdgesInitialToFinalCactusApSet( statement: CommonInst, initial: AccessPathWithCycles.AccessNode?, final: AccessWithExclusion - ): AccessWithExclusion? { + ): List> { val storage = sameInitialAccessEdges.getOrPut(initial) { EdgeNonUniverseExclusionMergingStorage(maxInstIdx, languageManager) } - return storage.add(statement, final) + return storage.add(statement, final)?.let(::listOf) ?: emptyList() } override fun filter( @@ -86,7 +86,10 @@ class MethodEdgesInitialToFinalCactusApSet( exclusions[edgeSetIdx] = mergedExclusion val mergedAccess = currentAccess.mergeAdd(accessWithExclusion.access) - if (mergedAccess === currentAccess) return null + if (mergedAccess === currentAccess) { + if (mergedExclusion === currentExclusion) return null + return AccessWithExclusion(currentAccess, mergedExclusion) + } edges[edgeSetIdx] = mergedAccess return AccessWithExclusion(mergedAccess, mergedExclusion) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSet.kt index 89b2f3762..3063c8d97 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSet.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSet.kt @@ -16,11 +16,12 @@ abstract class CommonF2FSet( data class AccessWithExclusion(val access: FAP, val exclusion: ExclusionSet) interface ApStorage { - fun add(statement: CommonInst, initial: IAP, final: AccessWithExclusion): AccessWithExclusion? + fun add(statement: CommonInst, initial: IAP, final: AccessWithExclusion): List> fun filter(dst: MutableList>>, statement: CommonInst, finalPattern: IAP) fun filter(dst: MutableList>, statement: CommonInst, initial: IAP, finalPattern: IAP) } + abstract fun createApStorage(): ApStorage private val storage = ExitFactBaseStorage() @@ -29,24 +30,41 @@ abstract class CommonF2FSet( statement: CommonInst, initialAp: InitialFactAp, finalAp: FinalFactAp, - ): Pair? { + ): List> = buildList { + addOne(statement, initialAp, finalAp) { addedInitial, addedFinal -> + add(addedInitial to addedFinal) + } + } + + override fun addAll( + statement: CommonInst, + initialAps: Iterable, + finalAp: FinalFactAp, + emitDelta: (InitialFactAp, FinalFactAp) -> Unit, + ) { + initialAps.forEach { initialAp -> addOne(statement, initialAp, finalAp, emitDelta) } + } + + private fun addOne( + statement: CommonInst, + initialAp: InitialFactAp, + finalAp: FinalFactAp, + emitDelta: (InitialFactAp, FinalFactAp) -> Unit, + ) { check(initialAp.exclusions == finalAp.exclusions) { "Edge exclusion mismatch" } val edgeStorage = storage.getOrCreate(finalAp.base).getOrCreate(initialAp.base) val final = AccessWithExclusion(getFinalAccess(finalAp), finalAp.exclusions) - val addedAccessWithExclusion = edgeStorage.add(statement, getInitialAccess(initialAp), final) - ?: return null - - if (addedAccessWithExclusion === final) return initialAp to finalAp - - val newInitialAp = createInitial(initialAp.base, getInitialAccess(initialAp), addedAccessWithExclusion.exclusion) - - val newExitAp = createFinal( - finalAp.base, addedAccessWithExclusion.access, addedAccessWithExclusion.exclusion - ) - - return newInitialAp to newExitAp + edgeStorage.add(statement, getInitialAccess(initialAp), final).forEach { added -> + if (added === final) { + emitDelta(initialAp, finalAp) + } else { + val newInitialAp = createInitial(initialAp.base, getInitialAccess(initialAp), added.exclusion) + val newExitAp = createFinal(finalAp.base, added.access, added.exclusion) + emitDelta(newInitialAp, newExitAp) + } + } } abstract fun mostAbstractPattern(base: AccessPathBase): IAP diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSummary.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSummary.kt index 602f02367..d4d6fafdf 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSummary.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/CommonF2FSummary.kt @@ -7,6 +7,7 @@ import org.opentaint.dataflow.ap.ifds.FactToFactEdgeBuilder import org.opentaint.dataflow.ap.ifds.MethodSummaryFactEdgesForExitPoint import org.opentaint.dataflow.ap.ifds.SummaryFactStorage import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialToFinalSummaryStorageStats import org.opentaint.dataflow.ap.ifds.access.MethodInitialToFinalApSummariesStorage import org.opentaint.dataflow.util.collectToListWithPostProcess import org.opentaint.ir.api.common.cfg.CommonInst @@ -19,6 +20,10 @@ abstract class CommonF2FSummary(val methodEntryPoint: CommonInst) interface Storage { fun add(edges: List>, added: MutableList>) fun collectSummariesTo(dst: MutableList>, initialFactPatter: FAP?) + fun collectSummariesByFinalTo(dst: MutableList>, finalFactPattern: FAP) { + collectSummariesTo(dst, initialFactPatter = null) + } + fun storageStats(): InitialToFinalSummaryStorageStats? = null } abstract fun createStorage(): Storage @@ -34,12 +39,29 @@ abstract class CommonF2FSummary(val methodEntryPoint: CommonInst) initialFactPattern: FinalFactAp?, finalFactBase: AccessPathBase? ) { - storage.filterEdgesTo(dst, EdgeStoragePattern(initialFactPattern, finalFactBase)) + storage.filterEdgesTo(dst, EdgeStoragePattern(initialFactPattern, finalFactBase, finalFactPattern = null)) + } + + override fun filterEdgesByFinalTo( + dst: MutableList, + finalFactPattern: FinalFactAp, + ) { + storage.filterEdgesTo( + dst, + EdgeStoragePattern( + initialFactPattern = null, + finalFactBase = finalFactPattern.base, + finalFactPattern = finalFactPattern, + ), + ) } + override fun storageStats(): InitialToFinalSummaryStorageStats? = storage.storageStats() + private class EdgeStoragePattern( val initialFactPattern: FinalFactAp?, - val finalFactBase: AccessPathBase? + val finalFactBase: AccessPathBase?, + val finalFactPattern: FinalFactAp?, ) private inner class MethodTaintedSummariesStorage : MethodSummaryFactEdgesForExitPoint(methodEntryPoint) { @@ -59,6 +81,10 @@ abstract class CommonF2FSummary(val methodEntryPoint: CommonInst) ) { storage.filterTo(dst, containsPattern) } + + fun storageStats(): InitialToFinalSummaryStorageStats? = sumStorageStats { body -> + forEachStorage { storage -> body(storage.storageStats()) } + } } private inner class MethodFactToFactSummaries : SummaryFactStorage(methodEntryPoint) { @@ -79,23 +105,42 @@ abstract class CommonF2FSummary(val methodEntryPoint: CommonInst) val initialFactBase = pattern.initialFactPattern?.base if (initialFactBase != null) { val storage = find(initialFactBase) ?: return - filterTo(dst, storage, initialFactBase, pattern.finalFactBase, getFinalAccess(pattern.initialFactPattern)) + filterTo( + dst, + storage, + initialFactBase, + pattern.finalFactBase, + getFinalAccess(pattern.initialFactPattern), + pattern.finalFactPattern?.let { getFinalAccess(it) }, + ) } else { forEachValue { base, storage -> - filterTo(dst, storage, base, pattern.finalFactBase, pattern.initialFactPattern?.let { getFinalAccess(it) }) + filterTo( + dst, + storage, + base, + pattern.finalFactBase, + pattern.initialFactPattern?.let { getFinalAccess(it) }, + pattern.finalFactPattern?.let { getFinalAccess(it) }, + ) } } } + fun storageStats(): InitialToFinalSummaryStorageStats? = sumStorageStats { body -> + forEachValue { _, storage -> body(storage.storageStats()) } + } + private fun filterTo( dst: MutableList, storage: MethodTaintedSummariesGroupedByFact, initialFactBase: AccessPathBase, finalFactBase: AccessPathBase?, - containsPattern: FAP? + containsPattern: FAP?, + finalFactPattern: FAP?, ) { collectToListWithPostProcess(dst, { - storage.filterEdgesTo(it, containsPattern, finalFactBase) + storage.filterEdgesTo(it, containsPattern, finalFactBase, finalFactPattern) }, { it.setInitialFactBase(initialFactBase).build() }) @@ -106,6 +151,10 @@ abstract class CommonF2FSummary(val methodEntryPoint: CommonInst) SummaryFactStorage>(methodEntryPoint) { override fun createStorage() = this@CommonF2FSummary.createStorage() + fun storageStats(): InitialToFinalSummaryStorageStats? = sumStorageStats { body -> + forEachValue { _, storage -> body(storage.storageStats()) } + } + fun add(edges: List, added: MutableList>) { val sameExitBaseEdges = edges.groupBy { it.factAp.base } for ((exitBase, sameBaseEdges) in sameExitBaseEdges) { @@ -128,14 +177,15 @@ abstract class CommonF2FSummary(val methodEntryPoint: CommonInst) fun filterEdgesTo( dst: MutableList>, containsPattern: FAP?, - finalFactBase: AccessPathBase? + finalFactBase: AccessPathBase?, + finalFactPattern: FAP?, ) { if (finalFactBase != null) { val storage = find(finalFactBase) ?: return - collectTo(dst, storage, finalFactBase, containsPattern) + collectTo(dst, storage, finalFactBase, containsPattern, finalFactPattern) } else { forEachValue { base, storage -> - collectTo(dst, storage, base, containsPattern) + collectTo(dst, storage, base, containsPattern, finalFactPattern) } } } @@ -144,14 +194,35 @@ abstract class CommonF2FSummary(val methodEntryPoint: CommonInst) dst: MutableList>, storage: Storage, finalFactBase: AccessPathBase, - containsPattern: FAP? + containsPattern: FAP?, + finalFactPattern: FAP?, ) = collectToListWithPostProcess(dst, { - storage.collectSummariesTo(it, containsPattern) + if (finalFactPattern == null) { + storage.collectSummariesTo(it, containsPattern) + } else { + storage.collectSummariesByFinalTo(it, finalFactPattern) + } }, { it.setExitFactBase(finalFactBase) }) } + private inline fun sumStorageStats( + collect: ((InitialToFinalSummaryStorageStats?) -> Unit) -> Unit, + ): InitialToFinalSummaryStorageStats? { + var supported = false + var edgeCount = 0L + var finalFactSizeSum = 0L + collect { stats -> + if (stats != null) { + supported = true + edgeCount += stats.edgeCount + finalFactSizeSum += stats.finalFactSizeSum + } + } + return if (supported) InitialToFinalSummaryStorageStats(edgeCount, finalFactSizeSum) else null + } + abstract class F2FBBuilder( private var initialBase: AccessPathBase? = null, private var exitBase: AccessPathBase? = null, diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/ndf2f/DefaultNDF2FSummaryStorageWithAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/ndf2f/DefaultNDF2FSummaryStorageWithAp.kt index b0112bf6d..086fa5345 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/ndf2f/DefaultNDF2FSummaryStorageWithAp.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/common/ndf2f/DefaultNDF2FSummaryStorageWithAp.kt @@ -14,9 +14,13 @@ abstract class DefaultNDF2FSummaryStorageWithAp( private val ap = arrayListOf() override fun initialApIdx(ap: InitialFactAp): Int = apIdx.getOrCreateIndex(ap.base, getInitialAccess(ap)) { + val idx = this.ap.size this.ap.add(ap) + initialApAdded(idx, ap) } + protected open fun initialApAdded(idx: Int, ap: InitialFactAp) = Unit + override fun getInitialApByIdx(idx: Int): InitialFactAp = ap[idx] override fun relevantInitialAp(summaryInitialFactPattern: FinalFactAp): BitSet = diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodEdgesInitialToFinalTreeApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodEdgesInitialToFinalTreeApSet.kt index 12164dfa6..4109a3dcb 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodEdgesInitialToFinalTreeApSet.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodEdgesInitialToFinalTreeApSet.kt @@ -28,10 +28,10 @@ class MethodEdgesInitialToFinalTreeApSet( statement: CommonInst, initial: AccessPath.AccessNode?, final: AccessWithExclusion, - ): AccessWithExclusion? { + ): List> { val storage = sameInitialAccessEdges.getOrCreateNode(initial).current - return storage.add(statement, final) + return storage.add(statement, final)?.let(::listOf) ?: emptyList() } override fun filter( diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallFlowFunction.kt index 6ccd5edd5..6df7e62f4 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallFlowFunction.kt @@ -10,6 +10,10 @@ import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem import org.opentaint.dataflow.taint.FinalFactReader interface MethodCallFlowFunction { + sealed interface FactToFactTransfer { + data object Unchanged : FactToFactTransfer + } + sealed interface CallFact sealed interface Call2ReturnFact @@ -88,6 +92,12 @@ interface MethodCallFlowFunction { fun propagateFactToFact(initialFactAp: InitialFactAp, currentFactAp: FinalFactAp): Set fun propagateNDFactToFact(initialFacts: Set, currentFactAp: FinalFactAp): Set + /** + * Returns a conclusion-only call transfer when the exact initial premise cannot affect the + * result, or null when the call must be evaluated once per exact premise. + */ + fun createFactToFactTransfer(currentFactAp: FinalFactAp): Set? = null + fun propagateZeroToZeroResolutionFailure(): Set fun propagateZeroToFactResolutionFailure(currentFactAp: FinalFactAp, startFactBase: AccessPathBase): Set fun propagateFactToFactResolutionFailure(initialFactAp: InitialFactAp, currentFactAp: FinalFactAp, startFactBase: AccessPathBase): Set diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSequentFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSequentFlowFunction.kt index 4a7585743..c684eef8b 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSequentFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSequentFlowFunction.kt @@ -1,5 +1,6 @@ package org.opentaint.dataflow.ap.ifds.analysis +import org.opentaint.dataflow.ap.ifds.Accessor import org.opentaint.dataflow.ap.ifds.SideEffectKind import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp @@ -7,6 +8,16 @@ import org.opentaint.dataflow.configuration.CommonTaintAction import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem interface MethodSequentFlowFunction { + sealed interface FactToFactTransfer { + data object Unchanged : FactToFactTransfer + data class Fact(val factAp: FinalFactAp, val traceInfo: TraceInfo?) : FactToFactTransfer + data class ExcludeAccessor( + val excludedFactAp: FinalFactAp, + val accessor: Accessor, + val traceInfo: TraceInfo?, + ) : FactToFactTransfer + } + sealed interface Sequent { data object Unchanged : Sequent data object ZeroToZero : Sequent @@ -32,4 +43,6 @@ interface MethodSequentFlowFunction { fun propagateZeroToFact(currentFactAp: FinalFactAp): Set fun propagateFactToFact(initialFactAp: InitialFactAp, currentFactAp: FinalFactAp): Set fun propagateNDFactToFact(initialFacts: Set, currentFactAp: FinalFactAp): Set -} \ No newline at end of file + + fun createFactToFactTransfer(currentFactAp: FinalFactAp): Set? = null +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/ForwardActionableRulesRecorder.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/ForwardActionableRulesRecorder.kt new file mode 100644 index 000000000..852f5c5c5 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/ForwardActionableRulesRecorder.kt @@ -0,0 +1,44 @@ +package org.opentaint.dataflow.ap.ifds.taint + +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.ir.api.common.cfg.CommonInst +import java.util.concurrent.ConcurrentHashMap + +typealias ActionableRules = + Map>> + +/** Source actions that emitted at least one fact during forward analysis. */ +class ForwardActionableRulesRecorder { + private val rules = ConcurrentHashMap< + CommonInst, + ConcurrentHashMap> + >() + + fun record( + statement: CommonInst, + rule: CommonTaintConfigurationItem, + action: CommonTaintAction, + ) { + rules.computeIfAbsent(statement) { ConcurrentHashMap() } + .computeIfAbsent(rule) { ConcurrentHashMap.newKeySet() } + .add(action) + } + + fun clear() = rules.clear() + + fun snapshot(): ActionableRules = rules.mapValues { (_, statementRules) -> + statementRules.mapValues { (_, actions) -> actions.toSet() } + } + + fun collectInto( + collector: MutableMap>>, + ) { + rules.forEach { (statement, statementRules) -> + val targetRules = collector.getOrPut(statement, ::hashMapOf) + statementRules.forEach { (rule, actions) -> + targetRules.getOrPut(rule, ::hashSetOf).addAll(actions) + } + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintAnalysisUnitStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintAnalysisUnitStorage.kt index e267df5e6..862d8c6dd 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintAnalysisUnitStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintAnalysisUnitStorage.kt @@ -3,6 +3,8 @@ package org.opentaint.dataflow.ap.ifds.taint import org.opentaint.dataflow.ap.ifds.LanguageManager import org.opentaint.dataflow.ap.ifds.MethodSummariesUnitStorage import org.opentaint.dataflow.ap.ifds.access.ApManager +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem import org.opentaint.ir.api.common.cfg.CommonInst import java.util.concurrent.ConcurrentHashMap @@ -15,10 +17,12 @@ class TaintAnalysisUnitStorage(apManager: ApManager, languageManager: LanguageMa ) private var vulnerabilityBuckets = ConcurrentHashMap() + private val forwardActionableRules = ForwardActionableRulesRecorder() override fun resetApManager(apManager: ApManager) { super.resetApManager(apManager) vulnerabilityBuckets = ConcurrentHashMap() + forwardActionableRules.clear() } fun addVulnerability(vulnerability: TaintSinkTracker.TaintVulnerability) { @@ -35,4 +39,14 @@ class TaintAnalysisUnitStorage(apManager: ApManager, languageManager: LanguageMa collector.add(it) } } + + fun recordForwardActionableRule( + statement: CommonInst, + rule: CommonTaintConfigurationItem, + action: CommonTaintAction, + ) = forwardActionableRules.record(statement, rule, action) + + fun collectForwardActionableRules( + collector: MutableMap>>, + ) = forwardActionableRules.collectInto(collector) } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintSinkTracker.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintSinkTracker.kt index 8c8874f9d..0edf05b16 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintSinkTracker.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/taint/TaintSinkTracker.kt @@ -9,12 +9,19 @@ import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker.TaintVulnerabilityR import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem import org.opentaint.dataflow.configuration.CommonTaintConfigurationSink import org.opentaint.dataflow.configuration.CommonTaintConfigurationSource +import org.opentaint.dataflow.configuration.CommonTaintAction import org.opentaint.ir.api.common.cfg.CommonInst import java.util.concurrent.ConcurrentHashMap class TaintSinkTracker( private val storage: TaintAnalysisUnitStorage, ) { + fun recordForwardActionableRule( + statement: CommonInst, + rule: CommonTaintConfigurationItem, + action: CommonTaintAction, + ) = storage.recordForwardActionableRule(statement, rule, action) + data class TaintVulnerability( val statement: CommonInst, val ruleId: String, diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/ExactProcessingTimeBudget.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/ExactProcessingTimeBudget.kt new file mode 100644 index 000000000..8251219c8 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/ExactProcessingTimeBudget.kt @@ -0,0 +1,85 @@ +package org.opentaint.dataflow.ap.ifds.trace + +import org.opentaint.dataflow.util.Cancellation +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds + +class ExactProcessingTimeBudget( + val limit: Duration, +) { + enum class Stage { + TRACE_RESOLUTION, + RULE_SEARCH, + } + + data class Snapshot( + val traceResolution: Duration, + val ruleSearch: Duration, + val limit: Duration, + ) { + val total: Duration get() = traceResolution + ruleSearch + val exhausted: Boolean get() = total >= limit + } + + data class Measurement( + val value: T, + val snapshot: Snapshot, + ) + + private class Counters { + val traceResolutionNanos = AtomicLong() + val ruleSearchNanos = AtomicLong() + + fun totalNanos(): Long = traceResolutionNanos.get() + ruleSearchNanos.get() + } + + private val limitNanos = limit.inWholeNanoseconds + private val counters = ConcurrentHashMap() + + init { + require(limit.isPositive() && limit.isFinite()) { "A finite positive time limit is required" } + } + + fun snapshot(key: K): Snapshot { + val current = counters[key] + return Snapshot( + traceResolution = (current?.traceResolutionNanos?.get() ?: 0L).nanoseconds, + ruleSearch = (current?.ruleSearchNanos?.get() ?: 0L).nanoseconds, + limit = limit, + ) + } + + fun isExhausted(key: K): Boolean = snapshot(key).exhausted + + fun measure( + key: K, + stage: Stage, + parentCancellation: Cancellation, + block: (Cancellation) -> T, + ): Measurement { + val counter = counters.computeIfAbsent(key) { Counters() } + val consumedAtStart = counter.totalNanos() + val startedAt = System.nanoTime() + val operationCancellation = parentCancellation.derive { + val currentOperationNanos = elapsedNanos(startedAt) + consumedAtStart + currentOperationNanos < limitNanos + } + + val value = try { + block(operationCancellation) + } finally { + val elapsed = elapsedNanos(startedAt) + when (stage) { + Stage.TRACE_RESOLUTION -> counter.traceResolutionNanos.addAndGet(elapsed) + Stage.RULE_SEARCH -> counter.ruleSearchNanos.addAndGet(elapsed) + } + } + + return Measurement(value, snapshot(key)) + } + + private fun elapsedNanos(startedAt: Long): Long = + (System.nanoTime() - startedAt).coerceAtLeast(0L) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/GraphReachabilityUtil.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/GraphReachabilityUtil.kt index 7dcef0230..831c27bf5 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/GraphReachabilityUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/GraphReachabilityUtil.kt @@ -20,3 +20,31 @@ inline fun entriesReachableFrom( } return false } + +/** + * Returns every entry that can reach at least one [target] through edges accepted by [edgeEntry]. + * The reverse graph is built once, so the cost is linear in the graph rather than one traversal + * per possible start entry. + */ +inline fun entriesThatCanReach( + successors: Map>, + target: Set, + edgeEntry: (Edge) -> T?, +): Set { + val predecessors = hashMapOf>() + for ((from, edges) in successors) { + for (edge in edges) { + val to = edgeEntry(edge) ?: continue + predecessors.getOrPut(to, ::arrayListOf).add(from) + } + } + + val reachable = hashSetOf() + val unprocessed = target.toMutableList() + while (unprocessed.isNotEmpty()) { + val entry = unprocessed.removeLast() + if (!reachable.add(entry)) continue + predecessors[entry]?.let(unprocessed::addAll) + } + return reachable +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodCallerSearchUtils.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodCallerSearchUtils.kt index 1d3230d9f..e757bcf17 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodCallerSearchUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodCallerSearchUtils.kt @@ -14,17 +14,20 @@ inline fun TaintAnalysisUnitRunnerManager.withMethodRunner( return runner.body() } -fun TaintAnalysisUnitRunnerManager.findMethodCallers(methodEntryPoint: MethodEntryPoint): Set { +fun TaintAnalysisUnitRunnerManager.findMethodCallers( + methodEntryPoint: MethodEntryPoint, + collectZeroCallsOnly: Boolean = true, +): Set { val result = hashSetOf() withMethodRunner(methodEntryPoint) { - methodCallers(methodEntryPoint, collectZeroCallsOnly = true, result) + methodCallers(methodEntryPoint, collectZeroCallsOnly, result) } val callers = methodCallers(methodEntryPoint.method) for (callerUnit in callers) { val runner = findUnitRunner(callerUnit) ?: error("No runner for unit: $callerUnit") - runner.methodCallers(methodEntryPoint, collectZeroCallsOnly = true, result) + runner.methodCallers(methodEntryPoint, collectZeroCallsOnly, result) } return result diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodForwardTraceResolver.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodForwardTraceResolver.kt index 0044b79e4..35d897829 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodForwardTraceResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodForwardTraceResolver.kt @@ -10,6 +10,7 @@ import org.opentaint.dataflow.ap.ifds.MethodEntryPoint import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication import org.opentaint.dataflow.ap.ifds.MethodWithContext +import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.analysis.AnalysisManager @@ -233,7 +234,11 @@ class MethodForwardTraceResolver( for (method in methodCalls) { when (method) { is MethodCallResolutionResult.ResolvedMethod -> { - for (ep in methodEntryPoints(method.method)) { + val analysisMethod = (analysisManager as? TaintAnalysisManager)?.overApproximateMethodContext( + method.method, + contextIndependentFact = callerEdge.factAp.base == AccessPathBase.ClassStatic, + ) ?: method.method + for (ep in methodEntryPoints(analysisMethod)) { handleMethodCall(ep, callerEdge, callerFact, startFactBase) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt index 06d5386eb..590a76534 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt @@ -15,9 +15,17 @@ import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdgeSearcher import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges import org.opentaint.dataflow.ap.ifds.MethodEntryPoint import org.opentaint.dataflow.ap.ifds.MethodWithContext +import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.ApManager +import org.opentaint.dataflow.ap.ifds.access.FactAp import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.ABSTRACT_EMPTY_ACCESS +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyFinalFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyInitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.eraseFieldForSummaryGeneralization import org.opentaint.dataflow.ap.ifds.analysis.AnalysisManager import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFactMapper @@ -44,8 +52,6 @@ import org.opentaint.dataflow.configuration.CommonTaintConfigurationSource import org.opentaint.dataflow.graph.MethodInstGraph import org.opentaint.dataflow.util.Cancellation import org.opentaint.dataflow.util.CompactIntSet -import org.opentaint.dataflow.util.ConcurrentReadSafeObject2IntMap -import org.opentaint.dataflow.util.ConcurrentReadSafeObject2IntMap.NO_VALUE import org.opentaint.dataflow.util.add import org.opentaint.dataflow.util.bitSetOf import org.opentaint.dataflow.util.cartesianProductMapTo @@ -54,8 +60,6 @@ import org.opentaint.dataflow.util.contains import org.opentaint.dataflow.util.forEach import org.opentaint.dataflow.util.forEachCartesianProduct import org.opentaint.dataflow.util.forEachIntEntry -import org.opentaint.dataflow.util.getOrCreateIndex -import org.opentaint.dataflow.util.object2IntMap import org.opentaint.dataflow.util.toBitSet import org.opentaint.ir.api.common.cfg.CommonAssignInst import org.opentaint.ir.api.common.cfg.CommonInst @@ -63,6 +67,36 @@ import org.opentaint.ir.api.common.cfg.CommonValue import java.util.BitSet import java.util.LinkedList import java.util.Objects +import java.util.concurrent.ConcurrentHashMap + +internal fun MethodTraceResolver.SummaryTrace.withUniverseExclusions(): MethodTraceResolver.SummaryTrace = + copy( + final = final.run { + copy( + edges = MethodTraceResolver.TraceEdges.conjoin( + edges.premisesByFinalFact.values.map { premises -> + MethodTraceResolver.TraceEdges.of(premises.map { it.withUniverseExclusions() }) + } + ) + ) + } + ) + +private fun MethodTraceResolver.TraceEdge.withUniverseExclusions(): MethodTraceResolver.TraceEdge = when (this) { + is MethodTraceResolver.TraceEdge.SourceTraceEdge -> MethodTraceResolver.TraceEdge.SourceTraceEdge( + fact.replaceExclusions(ExclusionSet.Universe) + ) + + is MethodTraceResolver.TraceEdge.MethodTraceEdge -> MethodTraceResolver.TraceEdge.MethodTraceEdge( + initialFact.replaceExclusions(ExclusionSet.Universe), + fact.replaceExclusions(ExclusionSet.Universe) + ) + + is MethodTraceResolver.TraceEdge.MethodTraceNDEdge -> MethodTraceResolver.TraceEdge.MethodTraceNDEdge( + initialFacts.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) }, + fact.replaceExclusions(ExclusionSet.Universe) + ) +} class MethodTraceResolver( private val runner: AnalysisRunner, @@ -70,13 +104,69 @@ class MethodTraceResolver( private val analysisContext: MethodAnalysisContext, private val edges: MethodAnalyzerEdges, private val graph: MethodInstGraph, + traceResolutionActionHardLimit: Int? = null, + private val cache: Cache = Cache(), ) { private val methodEntryPoint: MethodEntryPoint = analysisContext.methodEntryPoint private val analysisManager: AnalysisManager get() = runner.analysisManager private val manager: AnalysisUnitRunnerManager get() = runner.manager private val methodCallFactMapper: MethodCallFactMapper get() = analysisContext.methodCallFactMapper private val apManager: ApManager get() = runner.apManager + private val traceResolutionActionHardLimit = + traceResolutionActionHardLimit ?: TRACE_RESOLUTION_ACTION_HARD_LIMIT + + /** + * Query-independent data used while resolving traces for one analyzed method. + * + * A cache may be shared by concurrent resolvers only while the method edge storage is stable. + * [NormalMethodAnalyzer] owns one cache generation and replaces it with the method analysis state. + */ + class Cache internal constructor() { + private data class CallPassSummaryKey( + val currentEdge: TraceEdge, + val callee: MethodEntryPoint, + val startFact: CallPreconditionFact.CallToStart, + val statement: CommonInst, + ) + + private val entryEdgePresence = + ConcurrentHashMap>() + private val callPassSummaries = ConcurrentHashMap>() + private val calleeEntryPoints = ConcurrentHashMap>() + private val zeroEntryFacts = + ConcurrentHashMap>>() + + internal fun containsEntryEdge( + statement: CommonInst, + edge: TraceEdge, + compute: () -> Boolean, + ): Boolean = entryEdgePresence + .computeIfAbsent(statement) { ConcurrentHashMap() } + .computeIfAbsent(edge) { compute() } + + internal fun callPassSummaries( + currentEdge: TraceEdge, + callee: MethodEntryPoint, + startFact: CallPreconditionFact.CallToStart, + statement: CommonInst, + compute: () -> List, + ): List = callPassSummaries.computeIfAbsent( + CallPassSummaryKey(currentEdge, callee, startFact, statement) + ) { compute().toList() } + internal fun calleeEntryPoints( + statement: CommonInst, + compute: () -> List, + ): List = calleeEntryPoints.computeIfAbsent(statement) { compute().toList() } + + internal fun zeroEntryFacts( + statement: CommonInst, + base: AccessPathBase, + compute: () -> List, + ): List = zeroEntryFacts + .computeIfAbsent(statement) { ConcurrentHashMap() } + .computeIfAbsent(base) { compute().toList() } + } // Enum can give non-determinacy as its entries have new hash code on every JVM run. // Override hashcode() and equals() when using enum as a field in classes whose objects // can be stored in sets etc. @@ -117,6 +207,7 @@ class MethodTraceResolver( val startEntry: TraceEntry.StartTraceEntry, val final: TraceEntry.Final, val traceKind: TraceKind, + val isStartOverApproximation: Boolean = false, ) @Suppress("EqualsOrHashCode") @@ -151,6 +242,89 @@ class MethodTraceResolver( } } + /** + * A conjunction of requested final facts. Premises for the same final fact are alternatives; + * groups belonging to different final facts are conjunctive requirements. + */ + class TraceEdges private constructor( + val premisesByFinalFact: Map>, + private val flattened: Set, + ) : Set by flattened { + private val cachedHashCode = flattened.hashCode() + + init { + check(premisesByFinalFact.isNotEmpty() || flattened.isEmpty()) + check(premisesByFinalFact.values.all { it.isNotEmpty() }) + check(premisesByFinalFact.all { (fact, premises) -> premises.all { it.fact == fact } }) + check(flattened == premisesByFinalFact.values.flatten().toSet()) + } + + override fun equals(other: Any?): Boolean = + this === other || other is Set<*> && flattened == other + + override fun hashCode(): Int = cachedHashCode + + override fun toString(): String = premisesByFinalFact.toString() + + fun conjoin(other: TraceEdges): TraceEdges = conjoin(listOf(this, other)) + + fun collapseToFact(fact: InitialFactAp): TraceEdges { + if (premisesByFinalFact.size <= 1) return of(map { it.replaceFact(fact) }) + + val collapsedPremises = linkedSetOf() + val clauses = premisesByFinalFact.values.map { it.toList() } + clauses.forEachCartesianProduct { selectedPremises -> + val initialFacts = selectedPremises.flatMapTo(linkedSetOf()) { premise -> + when (premise) { + is TraceEdge.SourceTraceEdge -> emptySet() + is TraceEdge.MethodTraceEdge -> setOf(premise.initialFact) + is TraceEdge.MethodTraceNDEdge -> premise.initialFacts + } + } + collapsedPremises += when (initialFacts.size) { + 0 -> TraceEdge.SourceTraceEdge(fact) + 1 -> TraceEdge.MethodTraceEdge(initialFacts.single(), fact) + else -> TraceEdge.MethodTraceNDEdge(initialFacts, fact) + } + } + return of(collapsedPremises) + } + + companion object { + val Empty = TraceEdges(emptyMap(), emptySet()) + + fun of(edges: Iterable): TraceEdges { + val grouped = edges.groupByTo(linkedMapOf(), TraceEdge::fact) { it } + .mapValuesTo(linkedMapOf()) { (fact, premises) -> + premises.mapTo(linkedSetOf()) { it.canonicalize(fact) } + } + if (grouped.isEmpty()) return Empty + val flattened = grouped.values.flatMapTo(linkedSetOf()) { it } + return TraceEdges(grouped, flattened) + } + + fun conjoin(requirements: Iterable): TraceEdges { + val result = linkedMapOf>() + for (requirement in requirements) { + for ((fact, premises) in requirement.premisesByFinalFact) { + result.getOrPut(fact, ::linkedSetOf).addAll(premises) + } + } + return of(result.values.flatten()) + } + + private fun TraceEdge.canonicalize(fact: InitialFactAp): TraceEdge = when (this) { + is TraceEdge.SourceTraceEdge -> TraceEdge.SourceTraceEdge(fact) + is TraceEdge.MethodTraceEdge -> TraceEdge.MethodTraceEdge(initialFact, fact) + is TraceEdge.MethodTraceNDEdge -> when (initialFacts.size) { + 0 -> TraceEdge.SourceTraceEdge(fact) + 1 -> TraceEdge.MethodTraceEdge(initialFacts.single(), fact) + else -> TraceEdge.MethodTraceNDEdge(initialFacts, fact) + } + } + } + } + sealed interface TraceEntryAction { sealed interface PrimaryAction : TraceEntryAction @@ -164,8 +338,8 @@ class MethodTraceResolver( } sealed interface PassAction : TraceEntryAction { - val edges: Set - val edgesAfter: Set + val edges: TraceEdges + val edgesAfter: TraceEdges } sealed interface SourceAction : TraceEntryAction { @@ -179,9 +353,12 @@ class MethodTraceResolver( sealed interface SequentialAction: TraceEntryAction data class Sequential( - override val edges: Set, - override val edgesAfter: Set, - ) : SequentialAction, PrimaryAction, PassAction + override val edges: TraceEdges, + override val edgesAfter: TraceEdges, + ) : SequentialAction, PrimaryAction, PassAction { + constructor(edges: Set, edgesAfter: Set) : + this(TraceEdges.of(edges), TraceEdges.of(edgesAfter)) + } data class SequentialSourceRule( override val sourceEdges: Set, @@ -203,11 +380,18 @@ class MethodTraceResolver( ) : SourceOtherAction, CallRuleAction data class CallRule( - override val edges: Set, - override val edgesAfter: Set, + override val edges: TraceEdges, + override val edgesAfter: TraceEdges, override val rule: CommonTaintConfigurationItem, override val action: Set - ) : CallRuleAction, OtherAction, PassAction + ) : CallRuleAction, OtherAction, PassAction { + constructor( + edges: Set, + edgesAfter: Set, + rule: CommonTaintConfigurationItem, + action: Set, + ) : this(TraceEdges.of(edges), TraceEdges.of(edgesAfter), rule, action) + } sealed interface TraceSummaryEdge { val edge: TraceEdge @@ -233,13 +417,13 @@ class MethodTraceResolver( data class CallSummary( val summaryEdges: Set, val summaryTrace: SummaryTrace, - ) : CallAction, PrimaryAction, PassAction { - override val edges: Set - get() = summaryEdges.mapTo(hashSetOf()) { it.edge } - - override val edgesAfter: Set - get() = summaryEdges.mapTo(hashSetOf()) { it.edgeAfter } - } + override val edges: TraceEdges = TraceEdges.conjoin( + summaryEdges.map { TraceEdges.of(listOf(it.edge)) } + ), + override val edgesAfter: TraceEdges = TraceEdges.conjoin( + summaryEdges.map { TraceEdges.of(listOf(it.edgeAfter)) } + ), + ) : CallAction, PrimaryAction, PassAction data class CallSourceSummary( val summaryEdges: Set, @@ -250,35 +434,36 @@ class MethodTraceResolver( } data class UnresolvedCallSkip( - override val edges: Set, - override val edgesAfter: Set, - ) : CallAction, PrimaryAction, PassAction + override val edges: TraceEdges, + override val edgesAfter: TraceEdges, + ) : CallAction, PrimaryAction, PassAction { + constructor(edges: Set, edgesAfter: Set) : + this(TraceEdges.of(edges), TraceEdges.of(edgesAfter)) + } } data class ActionVariant( val primaryAction: PrimaryAction?, val otherActions: Set, - val unchanged: Set, + val unchanged: TraceEdges, ) { + constructor( + primaryAction: PrimaryAction?, + otherActions: Set, + unchanged: Set, + ) : this(primaryAction, otherActions, TraceEdges.of(unchanged)) + init { check(primaryAction != null || otherActions.isNotEmpty()) { "Entry is unchanged" } } - val edges: Set = buildSet { - addAll(unchanged) - - if (primaryAction is TraceEntryAction.PassAction) { - addAll(primaryAction.edges) - } - - for (otherAction in otherActions) { - if (otherAction is TraceEntryAction.PassAction) { - addAll(otherAction.edges) - } - } - } + val edges: TraceEdges = TraceEdges.conjoin(buildList { + add(unchanged) + if (primaryAction is TraceEntryAction.PassAction) add(primaryAction.edges) + otherActions.filterIsInstance().forEach { add(it.edges) } + }) private val cachedHashCode: Int = run { var result = primaryAction?.hashCode() ?: 0 @@ -303,23 +488,29 @@ class MethodTraceResolver( } sealed interface TraceEntry { - val edges: Set + val edges: TraceEdges val statement: CommonInst data class Action( - override val edges: Set, + override val edges: TraceEdges, override val statement: CommonInst, - ) : TraceEntry + ) : TraceEntry { + constructor(edges: Set, statement: CommonInst) : this(TraceEdges.of(edges), statement) + } data class Unchanged( - override val edges: Set, + override val edges: TraceEdges, override val statement: CommonInst, - ) : TraceEntry + ) : TraceEntry { + constructor(edges: Set, statement: CommonInst) : this(TraceEdges.of(edges), statement) + } data class Final( - override val edges: Set, + override val edges: TraceEdges, override val statement: CommonInst - ) : TraceEntry + ) : TraceEntry { + constructor(edges: Set, statement: CommonInst) : this(TraceEdges.of(edges), statement) + } sealed interface StartTraceEntry: TraceEntry @@ -327,9 +518,9 @@ class MethodTraceResolver( val facts: Set, val entryPoint: MethodEntryPoint, ) : StartTraceEntry { - override val edges: Set get() = facts.mapTo(hashSetOf()) { + override val edges: TraceEdges get() = TraceEdges.of(facts.mapTo(hashSetOf()) { TraceEdge.MethodTraceEdge(it, it) - } + }) override val statement: CommonInst get() = entryPoint.statement @@ -340,14 +531,14 @@ class MethodTraceResolver( val sourceOtherActions: Set, override val statement: CommonInst, ) : StartTraceEntry { - override val edges: Set get() = buildSet { + override val edges: TraceEdges get() = TraceEdges.of(buildSet { sourcePrimaryAction?.let { addAll(it.sourceEdges) } sourceOtherActions.forEach { addAll(it.sourceEdges) } - } + }) } } - private class EntryManager { + internal class EntryManager { val entries = arrayListOf() private val entryId = Object2IntOpenHashMap().apply { defaultReturnValue(NO_ENTRY) } @@ -376,12 +567,15 @@ class MethodTraceResolver( ) { val entryManager = EntryManager() val finalEntryId: Int = entryManager.entryId(finalEntry) + val finalHasAlternativePremises = finalEntry.edges.premisesByFinalFact.values.any { it.size > 1 } val startEntryIds = BitSet() var processedEntryIds = CompactIntSet().also { it.add(finalEntryId) } val unprocessedEntryIds = IntArrayList().also { it.add(finalEntryId) } val predecessors = Int2ObjectOpenHashMap() val successors = Int2ObjectOpenHashMap() + var steps = 0 + var actionHardLimitReached = false fun addPredecessor(current: TraceEntry, predecessor: TraceEntry, enqueue: Boolean = true) { val currentId = entryManager.entryId(current) @@ -418,7 +612,7 @@ class MethodTraceResolver( fun createAction( statement: CommonInst, - edges: Set, + edges: TraceEdges, variants: Set, ): TraceEntry { val action = TraceEntry.Action(edges, statement) @@ -462,13 +656,25 @@ class MethodTraceResolver( includeStatement: Boolean ): List { val traceKind = if (includeStatement) TraceKind.TraceToFactAfterStatement else TraceKind.TraceToFact - - val result = mutableListOf() - this.cartesianProductMapTo { - val finalEntry = TraceEntry.Final(it.toHashSet(), statement) - result += SummaryTrace(methodEntryPoint, finalEntry, traceKind) + if (any { it.isEmpty() }) return emptyList() + + if (apManager !is BaseOnlyApManager) { + val result = mutableListOf() + cartesianProductMapTo { selectedPremises -> + result += SummaryTrace( + methodEntryPoint, + TraceEntry.Final(selectedPremises.toHashSet(), statement), + traceKind, + ) + } + return result } - return result + + val finalEntry = TraceEntry.Final( + TraceEdges.conjoin(map { TraceEdges.of(it) }), + statement, + ) + return listOf(SummaryTrace(methodEntryPoint, finalEntry, traceKind)) } private fun resolveIntraProceduralTraceEdge( @@ -537,16 +743,177 @@ class MethodTraceResolver( } } + /** + * Resolves only the information needed to connect an inter-procedural summary to a method start. + * + * A summary with a non-Zero premise already carries its method-entry facts, so reconstructing the + * intra-procedural path cannot add information to [Start2FinalTrace]. This also covers mixed + * summaries: their Zero premises are produced inside the method while their non-Zero premises + * determine the method start. For an all-Zero summary, the resolver walks the CFG forward and + * stops each path at its first Z2F edge carrying the requested mark. Backward resolution then + * starts at that frontier instead of at the summary final. The exact resolver remains the + * completeness fallback for summaries for which the frontier cannot produce a source start. + */ + fun resolveIntraProceduralOverApproximateStart2FinalTrace( + summaryTrace: SummaryTrace, + cancellation: Cancellation, + ): List { + val st = summaryTrace.withUniverseExclusions() + check(st.method == methodEntryPoint) { "Incorrect summary trace" } + + if (st.final.edges.premisesByFinalFact.values.any { it.size > 1 }) { + return resolveIntraProceduralStart2FinalTrace(st, cancellation) + } + + val premises = st.final.summaryPremises() + if (premises.nonZeroFacts.isNotEmpty()) { + val methodEntryFacts = premises.nonZeroFacts + val start = TraceEntry.MethodEntry(methodEntryFacts, methodEntryPoint) + return listOf( + Start2FinalTrace( + methodEntryPoint, + start, + st.final, + st.traceKind, + isStartOverApproximation = true, + ) + ) + } + if (!premises.hasZero) { + return resolveIntraProceduralStart2FinalTrace(st, cancellation) + } + + val requestedFactsByMark = st.final.edges + .filterIsInstance() + .flatMap { edge -> edge.fact.taintMarks().map { mark -> mark to edge.fact } } + .groupBy({ it.first }, { it.second }) + + if (requestedFactsByMark.isEmpty()) { + return resolveIntraProceduralStart2FinalTrace(st, cancellation) + } + + val starts = hashSetOf() + for ((mark, requestedFacts) in requestedFactsByMark) { + val origins = findFirstZeroFactOrigins(mark, cancellation) + val originQueries = buildSet { + for (origin in origins) { + origin.rebaseRequestedFacts(requestedFacts).forEach { pattern -> + add(origin.statement to pattern) + } + } + } + for ((originStatement, originPattern) in originQueries) { + val originTrace = SummaryTrace( + method = methodEntryPoint, + final = TraceEntry.Final( + edges = setOf(TraceEdge.SourceTraceEdge(originPattern)), + statement = originStatement, + ), + traceKind = TraceKind.TraceToFactAfterStatement, + ) + + val prefixTraces = resolveIntraProceduralStart2FinalTrace(originTrace, cancellation) + prefixTraces.mapNotNullTo(starts) { it.startEntry as? TraceEntry.SourceStartEntry } + } + } + + if (starts.isEmpty()) { + return resolveIntraProceduralStart2FinalTrace(st, cancellation) + } + + return starts.map { start -> + Start2FinalTrace( + methodEntryPoint, + start, + st.final, + st.traceKind, + isStartOverApproximation = true, + ) + } + } + + private data class SummaryPremises( + val hasZero: Boolean, + val nonZeroFacts: Set, + ) + + private fun TraceEntry.Final.summaryPremises(): SummaryPremises { + var hasZero = false + val nonZeroFacts = hashSetOf() + for (edge in edges) { + when (edge) { + is TraceEdge.SourceTraceEdge -> hasZero = true + is TraceEdge.MethodTraceEdge -> nonZeroFacts += edge.initialFact + is TraceEdge.MethodTraceNDEdge -> nonZeroFacts += edge.initialFacts + } + } + return SummaryPremises(hasZero, nonZeroFacts) + } + + private data class ZeroFactOrigin( + val statement: CommonInst, + val fact: FinalFactAp, + ) + + private fun findFirstZeroFactOrigins( + mark: TaintMarkAccessor, + cancellation: Cancellation, + ): List { + val result = arrayListOf() + val visited = BitSet(graph.instructions.size) + val unprocessed = IntArrayList() + unprocessed.add(analysisManager.getInstIndex(methodEntryPoint.statement)) + + while (unprocessed.isNotEmpty() && cancellation.isActive()) { + val statementIdx = unprocessed.removeInt(unprocessed.lastIndex) + if (!visited.add(statementIdx)) continue + + val statement = graph.instructions[statementIdx] + val matchingFacts = edges.allZeroToFactFactsAtStatement(statement) + .filter { mark in it.taintMarks() } + if (matchingFacts.isNotEmpty()) { + matchingFacts.forEach { result += ZeroFactOrigin(statement, it) } + continue + } + + graph.graph.forEachSuccessor(statementIdx) { successor -> + if (!visited.get(successor)) unprocessed.add(successor) + } + } + + return result + } + + private fun ZeroFactOrigin.rebaseRequestedFacts( + requestedFacts: List, + ): Set = requestedFacts.mapTo(hashSetOf()) { requested -> + requested.rebase(fact.base).replaceExclusions(ExclusionSet.Universe) + } + + private fun FactAp.taintMarks(): Set = + getAllAccessors().filterIsInstanceTo(hashSetOf()) + fun resolveIntraProceduralStart2FinalTrace( summaryTrace: SummaryTrace, cancellation: Cancellation, ): List { - val st = summaryTrace.universeTrace() + val st = summaryTrace.withUniverseExclusions() check(st.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(st.final, cancellation, collectActionVariants = false) + val builder = TraceBuilder( + st.final, + cancellation, + collectActionVariants = false, + ) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps + if (!cancellation.isActive()) return emptyList() + + if (builder.actionHardLimitReached && st.final.edges.hasAlternativePremises()) { + return st.resolveExactCubes(cancellation::isActive) { cube -> + resolveIntraProceduralStart2FinalTrace(cube, cancellation) + } + } val traces = mutableListOf() builder.startEntryIds.forEach { startEntryId -> @@ -561,17 +928,34 @@ class MethodTraceResolver( cancellation: Cancellation, collapseUnchangedNodes: Boolean ): List { - val st = summaryTrace.universeTrace() + val st = summaryTrace.withUniverseExclusions() check(st.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(st.final, cancellation, collectActionVariants = true) + val builder = TraceBuilder( + st.final, + cancellation, + collectActionVariants = true, + ) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps + if (!cancellation.isActive()) return emptyList() + + if (builder.actionHardLimitReached && st.final.edges.hasAlternativePremises()) { + return st.resolveExactCubes(cancellation::isActive) { cube -> + resolveIntraProceduralFullStart2FinalTrace( + cube, + cancellation, + collapseUnchangedNodes, + ) + } + } builder.removeUnreachableNodes() + if (!cancellation.isActive()) return emptyList() if (collapseUnchangedNodes) { builder.collapseUnchangedNodes() } + if (!cancellation.isActive()) return emptyList() val fullTrace = builder.fullTrace(st.traceKind) return fullTrace } @@ -583,27 +967,87 @@ class MethodTraceResolver( ): List { check(start2FinalTrace.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(start2FinalTrace.final, cancellation, collectActionVariants = true) + val builder = TraceBuilder( + start2FinalTrace.final, + cancellation, + collectActionVariants = true, + ) builder.resolveTrace(start2FinalTrace.traceKind) stats.traceResolverSteps += builder.steps + if (!cancellation.isActive()) return emptyList() - val requiredStartId = builder.entryManager.entryId(start2FinalTrace.startEntry) - if (!builder.startEntryIds.contains(requiredStartId)) { - logger.warn("Trace start entry to found for: $methodEntryPoint") - return emptyList() + if (builder.actionHardLimitReached && start2FinalTrace.final.edges.hasAlternativePremises()) { + return start2FinalTrace.resolveExactFullCubes( + cancellation, + collapseUnchangedNodes, + ) } - builder.startEntryIds.clear() - builder.startEntryIds.set(requiredStartId) + if (!start2FinalTrace.isStartOverApproximation) { + val requiredStartId = builder.entryManager.entryId(start2FinalTrace.startEntry) + if (!builder.startEntryIds.contains(requiredStartId)) { + logger.warn("Trace start entry to found for: $methodEntryPoint") + return emptyList() + } + + builder.startEntryIds.clear() + builder.startEntryIds.set(requiredStartId) + } builder.removeUnreachableNodes() + if (!cancellation.isActive()) return emptyList() if (collapseUnchangedNodes) { builder.collapseUnchangedNodes() } + if (!cancellation.isActive()) return emptyList() val fullTrace = builder.fullTrace(start2FinalTrace.traceKind) return fullTrace } + private fun Start2FinalTrace.resolveExactFullCubes( + cancellation: Cancellation, + collapseUnchangedNodes: Boolean, + ): List { + val result = mutableListOf() + final.forEachExactCube { cube -> + if (!cancellation.isActive()) return result + val cubeTrace = SummaryTrace(method, cube, traceKind) + val resolved = resolveIntraProceduralFullStart2FinalTrace( + cubeTrace, + cancellation, + collapseUnchangedNodes, + ) + if (isStartOverApproximation) { + result += resolved + } else { + resolved.filterTo(result) { it.startEntry == startEntry } + } + } + return result + } + + private fun TraceEdges.hasAlternativePremises(): Boolean = + premisesByFinalFact.values.any { it.size > 1 } + + private inline fun SummaryTrace.resolveExactCubes( + isActive: () -> Boolean, + resolve: (SummaryTrace) -> List, + ): List { + val result = mutableListOf() + final.forEachExactCube { + if (!isActive()) return result + result += resolve(copy(final = it)) + } + return result + } + + private inline fun TraceEntry.Final.forEachExactCube(body: (TraceEntry.Final) -> Unit) { + val clauses = edges.premisesByFinalFact.values.map { it.toList() } + clauses.forEachCartesianProduct { selectedPremises -> + body(copy(edges = TraceEdges.of(selectedPremises.asIterable()))) + } + } + private fun TraceBuilder.removeUnreachableNodes() { val reachableFromStart = BitSet() val reachableFromFinish = BitSet() @@ -640,7 +1084,7 @@ class MethodTraceResolver( private inline fun TraceBuilder.traverseReachableNodes(reachable: BitSet, initial: BitSet, next: (Int) -> CompactIntSet) { initial.forEach { unprocessedEntryIds.add(it) } - while (unprocessedEntryIds.isNotEmpty()) { + while (unprocessedEntryIds.isNotEmpty() && cancellation.isActive()) { steps++ val entryId = unprocessedEntryIds.removeInt(unprocessedEntryIds.lastIndex) @@ -655,7 +1099,7 @@ class MethodTraceResolver( processedEntryIds = CompactIntSet() unprocessedEntryIds.add(finalEntryId) - while (unprocessedEntryIds.isNotEmpty()) { + while (unprocessedEntryIds.isNotEmpty() && cancellation.isActive()) { val entryId = unprocessedEntryIds.removeInt(unprocessedEntryIds.lastIndex) if (processedEntryIds.contains(entryId)) continue @@ -708,17 +1152,21 @@ class MethodTraceResolver( private fun TraceBuilder.fullTrace(traceKind: TraceKind): List { val allSuccessors = successors() + if (!cancellation.isActive()) return emptyList() val result = mutableListOf() startEntryIds.forEach { entryId: Int -> + if (!cancellation.isActive()) return@forEach val mapper = EntryMapper(entryManager) val finalEntry = mapper.translate(finalEntryId) val startEntry = mapper.translate(entryId) - val successors = mapper.translateSuccessors(entryId, allSuccessors) + val successors = mapper.translateSuccessors(entryId, allSuccessors, cancellation) + if (!cancellation.isActive()) return@forEach val entries = mapper.entries.toTypedArray() val actionVariants = Int2ObjectOpenHashMap>() unsafeActionVariants().forEachIntEntry { key, value -> + if (!cancellation.isActive()) return@forEachIntEntry if (!mapper.isTranslated(key)) return@forEachIntEntry val translatedId = mapper.translate(key) @@ -730,18 +1178,19 @@ class MethodTraceResolver( ) } - return result + return result.takeIf { cancellation.isActive() }.orEmpty() } private fun EntryMapper.translateSuccessors( start: Int, - allSuccessors: Int2ObjectOpenHashMap + allSuccessors: Int2ObjectOpenHashMap, + cancellation: Cancellation, ): Int2ObjectOpenHashMap { val result = Int2ObjectOpenHashMap() val unprocessed = IntArrayList() unprocessed.add(start) - while (unprocessed.isNotEmpty()) { + while (unprocessed.isNotEmpty() && cancellation.isActive()) { val node = unprocessed.removeInt(unprocessed.lastIndex) val translatedNode = translate(node) @@ -762,6 +1211,7 @@ class MethodTraceResolver( private fun TraceBuilder.successors(): Int2ObjectOpenHashMap { val allSuccessors = Int2ObjectOpenHashMap() for ((entryId, entryPredecessorIds) in predecessors) { + if (!cancellation.isActive()) break entryPredecessorIds.forEach { predecessorId: Int -> allSuccessors.computeIfAbsent(predecessorId) { CompactIntSet() }.add(entryId) } @@ -771,8 +1221,15 @@ class MethodTraceResolver( private fun TraceBuilder.resolveTrace(traceKind: TraceKind) { while (unprocessedEntryIds.isNotEmpty() && cancellation.isActive()) { - if (actions() > TRACE_RESOLUTION_ACTION_HARD_LIMIT && !startEntryIds.isEmpty) { - logger.warn { "Trace resolution stopped for $methodEntryPoint: hard limit" } + if ( + actions() > traceResolutionActionHardLimit && + (finalHasAlternativePremises || !startEntryIds.isEmpty) + ) { + actionHardLimitReached = true + logger.warn { + "Trace resolution stopped for $methodEntryPoint: action hard limit " + + traceResolutionActionHardLimit + } return } @@ -816,45 +1273,45 @@ class MethodTraceResolver( private fun TraceBuilder.propagateEntryToMethodEntryPoint( entry: TraceEntry ) { - val entryEdges = hashSetOf() - val sources = hashSetOf() - - for (edge in entry.edges) { - // We always have fact before entry point - if (!containsEntryEdge(entry.statement, edge)) return - - when (edge) { - is TraceEdge.MethodTraceEdge -> { - entryEdges.add(edge) - } - - is TraceEdge.MethodTraceNDEdge -> { - entryEdges.add(edge) - } - - is TraceEdge.SourceTraceEdge -> { - val preconditionFunction = analysisManager.getMethodStartPrecondition(apManager, analysisContext) - preconditionFunction.factPrecondition(edge.fact).forEach { - val source = TraceEntryAction.EntryPointSourceRule( - setOf(edge), methodEntryPoint, it.rule, it.action - ) - sources.add(source) + val applicablePremises = entry.edges.premisesByFinalFact.values.map { premises -> + premises.filter { containsEntryEdgeCached(entry.statement, it) } + } + if (applicablePremises.any { it.isEmpty() }) return + + applicablePremises.forEachCartesianProduct { selectedPremises -> + val entryEdges = hashSetOf() + val sources = hashSetOf() + + for (edge in selectedPremises) { + when (edge) { + is TraceEdge.MethodTraceEdge -> entryEdges.add(edge) + is TraceEdge.MethodTraceNDEdge -> entryEdges.add(edge) + is TraceEdge.SourceTraceEdge -> { + val preconditionFunction = + analysisManager.getMethodStartPrecondition(apManager, analysisContext) + preconditionFunction.factPrecondition(edge.fact).forEach { + sources += TraceEntryAction.EntryPointSourceRule( + setOf(edge), methodEntryPoint, it.rule, it.action + ) + } } } } - } - if (entryEdges.isEmpty()) { - if (sources.isEmpty()) return + if (entryEdges.isEmpty()) { + if (sources.isNotEmpty()) { + addPredecessor( + entry, + TraceEntry.SourceStartEntry(null, sources, methodEntryPoint.statement) + ) + } + return@forEachCartesianProduct + } - addPredecessor( - entry, - TraceEntry.SourceStartEntry(sourcePrimaryAction = null, sources, methodEntryPoint.statement) - ) - } else { val preStartEntry = if (sources.isNotEmpty()) { - val actionVariant = ActionVariant(primaryAction = null, sources, entryEdges) - createAction(methodEntryPoint.statement, entryEdges, setOf(actionVariant)) + val entryRequirements = TraceEdges.of(entryEdges) + val actionVariant = ActionVariant(primaryAction = null, sources, entryRequirements) + createAction(methodEntryPoint.statement, entryRequirements, setOf(actionVariant)) .also { addPredecessor(entry, it, enqueue = false) } } else { entry @@ -868,13 +1325,12 @@ class MethodTraceResolver( } } - val startEntry = TraceEntry.MethodEntry(entryFacts, methodEntryPoint) - addPredecessor(preStartEntry, startEntry) + addPredecessor(preStartEntry, TraceEntry.MethodEntry(entryFacts, methodEntryPoint)) } } private sealed interface ActionOrUnchanged { - data class Unchanged(val edge: TraceEdge) : ActionOrUnchanged + data class Unchanged(val edges: TraceEdges) : ActionOrUnchanged data class Action(val action: T) : ActionOrUnchanged } @@ -896,22 +1352,33 @@ class MethodTraceResolver( val callEdges = mutableListOf>>() - for (edge in entry.edges) { - val preconditions = callFactPrecondition(preconditionFunction, edge.fact, callees) + for ((fact, currentEdges) in entry.edges.premisesByFinalFact) { + val preconditions = callFactPrecondition(preconditionFunction, fact, callees) val callActions = mutableListOf>() for (precondition in preconditions) { when (precondition) { - is CallPrecondition.Unchanged -> callActions += ActionOrUnchanged.Unchanged(edge) + is CallPrecondition.Unchanged -> { + callActions += ActionOrUnchanged.Unchanged(TraceEdges.of(currentEdges)) + } is MethodCallPrecondition.PreconditionFactsForInitialFact -> { - val initialEdge = edge.replaceFact(precondition.initialFact) - if (!skipFactCheck && !containsEntryEdge(entry.statement, initialEdge)) { - continue + val applicableEdges = if (skipFactCheck) { + currentEdges + } else { + currentEdges.filterTo(hashSetOf()) { + containsEntryEdgeCached(entry.statement, it.replaceFact(precondition.initialFact)) + } } + if (applicableEdges.isEmpty()) continue collectToListWithPostProcess( callActions, - { it.propagateCall(edge, precondition.preconditionFacts) }, + { + it.propagateCall( + TraceEdges.of(applicableEdges), + precondition.preconditionFacts, + ) + }, { ActionOrUnchanged.Action(it) } ) } @@ -925,24 +1392,25 @@ class MethodTraceResolver( callEdges.add(callActions) } - val allUnchanged = callEdges.allUnchanged() if (allUnchanged != null) { addPredecessor(entry, TraceEntry.Unchanged(allUnchanged, statement)) return } - val resolvedMethods by lazy { - callees.mapNotNull { - when (it) { - is MethodCallResolutionResult.ResolvedMethod -> it.method - MethodCallResolutionResult.ResolutionFailure -> null - } + val resolvedMethodEntryPoints by lazy { + cache.calleeEntryPoints(statement) { + callees.mapNotNull { + when (it) { + is MethodCallResolutionResult.ResolvedMethod -> it.method + MethodCallResolutionResult.ResolutionFailure -> null + } + }.flatMap(::methodEntryPoints) } } val resolvedCallActions = mutableListOf() - forEachMergedCallActionsCombination(callEdges, resolvedMethods) { callAction -> + forEachMergedCallActionsCombination(callEdges, { resolvedMethodEntryPoints }) { callAction -> resolvedCallActions.resolveCallAction(preconditionFunction, statement, callAction) } @@ -954,33 +1422,44 @@ class MethodTraceResolver( val sequentActions = mutableListOf>>() - for (edge in entry.edges) { - val preconditions = preconditionFunction.factPrecondition(edge.fact) + for ((fact, currentEdges) in entry.edges.premisesByFinalFact) { + val preconditions = preconditionFunction.factPrecondition(fact) val actions = mutableListOf>() for (precondition in preconditions) { when (precondition) { - is SequentPrecondition.Unchanged -> actions += ActionOrUnchanged.Unchanged(edge) + is SequentPrecondition.Unchanged -> { + actions += ActionOrUnchanged.Unchanged(TraceEdges.of(currentEdges)) + } is MethodSequentPrecondition.SequentPreconditionFacts -> { - val initialEdge = edge.replaceFact(precondition.fact) - if (!skipFactCheck && !containsEntryEdge(entry.statement, initialEdge)) { - continue + val applicableEdges = if (skipFactCheck) { + currentEdges + } else { + currentEdges.filterTo(hashSetOf()) { + containsEntryEdgeCached(entry.statement, it.replaceFact(precondition.fact)) + } } + if (applicableEdges.isEmpty()) continue when (precondition) { is MethodSequentPrecondition.PreconditionFactsForInitialFact -> { precondition.preconditionFacts.mapTo(actions) { fact -> ActionOrUnchanged.Action( - TraceEntryAction.Sequential(setOf(edge.replaceFact(fact)), setOf(edge)) + TraceEntryAction.Sequential( + TraceEdges.of(applicableEdges.map { it.replaceFact(fact) }), + TraceEdges.of(applicableEdges), + ) ) } } is MethodSequentPrecondition.SequentSource -> { - if (initialEdge is TraceEdge.SourceTraceEdge) { + val sourceEdges = applicableEdges + .filterIsInstanceTo(hashSetOf()) + if (sourceEdges.isNotEmpty()) { actions += ActionOrUnchanged.Action( TraceEntryAction.SequentialSourceRule( - setOf(initialEdge), precondition.rule.rule, precondition.rule.action + sourceEdges, precondition.rule.rule, precondition.rule.action ) ) } @@ -1062,7 +1541,7 @@ class MethodTraceResolver( entry: TraceEntry, statement: CommonInst, ) { - val variantsByEdges = hashMapOf, MutableSet>() + val variantsByEdges = hashMapOf>() for (sequent in actionsCombination) { if (sequent.other.isEmpty()) { @@ -1073,7 +1552,10 @@ class MethodTraceResolver( val primaryUnchanged = sequent.primary.canBeTreatedAsUnchanged() if (primaryUnchanged != null) { - addPredecessor(entry, TraceEntry.Unchanged(sequent.unchanged + primaryUnchanged, statement)) + addPredecessor( + entry, + TraceEntry.Unchanged(sequent.unchanged.conjoin(primaryUnchanged), statement), + ) continue } } @@ -1093,7 +1575,7 @@ class MethodTraceResolver( } } - private fun PrimaryAction.canBeTreatedAsUnchanged(): Set? { + private fun PrimaryAction.canBeTreatedAsUnchanged(): TraceEdges? { if (this !is TraceEntryAction.PassAction) return null if (this !is CallSummary && this !is TraceEntryAction.Sequential) return null @@ -1107,17 +1589,17 @@ class MethodTraceResolver( val after = edgesAfter.singleOrNull() ?: return null if (edge != after) return null - return setOf(edge) + return TraceEdges.of(setOf(edge)) } - private fun List>>.allUnchanged(): Set? { - val unchanged = hashSetOf() + private fun List>>.allUnchanged(): TraceEdges? { + val unchanged = mutableListOf() for (aouGroup in this) { val aou = aouGroup.singleOrNull() ?: return null if (aou !is ActionOrUnchanged.Unchanged) return null - unchanged.add(aou.edge) + unchanged += aou.edges } - return unchanged + return TraceEdges.conjoin(unchanged) } private fun tryCreateSourceStart( @@ -1136,7 +1618,7 @@ class MethodTraceResolver( } private data class ActionEdgeCombination( - val unchanged: Set, + val unchanged: TraceEdges, val primary: PrimaryAction?, val other: Set, ) @@ -1144,22 +1626,22 @@ class MethodTraceResolver( private fun mergeSequentEdgeCombinations(allActions: List>>): List { val result = mutableListOf() allActions.cartesianProductMapTo { actionCombination -> - val unchanged = hashSetOf() - val sequential = hashSetOf() - val sequentialAfter = hashSetOf() + val unchanged = mutableListOf() + val sequential = mutableListOf() + val sequentialAfter = mutableListOf() val rules = hashSetOf() for (aou in actionCombination) { when (aou) { is ActionOrUnchanged.Unchanged -> { - unchanged.add(aou.edge) + unchanged += aou.edges } is ActionOrUnchanged.Action -> when (val action = aou.action) { is TraceEntryAction.Sequential -> { - sequential.addAll(action.edges) - sequentialAfter.addAll(action.edgesAfter) + sequential += action.edges + sequentialAfter += action.edgesAfter } is TraceEntryAction.SequentialSourceRule -> rules.add(action) @@ -1167,34 +1649,42 @@ class MethodTraceResolver( } } - val primaryAction = sequential.takeIf { it.isNotEmpty() }?.let { TraceEntryAction.Sequential(it, sequentialAfter) } - result += ActionEdgeCombination(unchanged, primaryAction, rules) + val primaryAction = sequential.takeIf { it.isNotEmpty() }?.let { + TraceEntryAction.Sequential( + TraceEdges.conjoin(it), + TraceEdges.conjoin(sequentialAfter), + ) + } + result += ActionEdgeCombination(TraceEdges.conjoin(unchanged), primaryAction, rules) } return result } private data class PartialCallEdgeCombination( - val unchanged: Set, + val unchanged: TraceEdges, val primary: PartiallyResolvedMergedPrimaryCallAction?, val rule: Set, ) private inline fun forEachMergedCallActionsCombination( callActions: List>>, - callees: List, + noinline calleeEntryPoints: () -> List, body: (PartialCallEdgeCombination) -> Unit, ) { + val seen = hashSetOf() callActions.forEachCartesianProduct { actions -> - val mergedActions = mergeCallActions(actions) { callees } - mergedActions.forEach(body) + val mergedActions = mergeCallActions(actions, calleeEntryPoints) + mergedActions.forEach { action -> + if (seen.add(action)) body(action) + } } } private fun mergeCallActions( aouGroup: Array>, - resolveMethodCallees: () -> List + resolveCalleeEntryPoints: () -> List, ): List { - val unchanged = hashSetOf() + val unchanged = mutableListOf() val rules = hashSetOf() val summary = hashSetOf() val unresolvedSkips = hashSetOf() @@ -1202,7 +1692,7 @@ class MethodTraceResolver( for (aou in aouGroup) { when (aou) { is ActionOrUnchanged.Unchanged -> { - unchanged.add(aou.edge) + unchanged += aou.edges } is ActionOrUnchanged.Action -> when (val action = aou.action) { @@ -1217,12 +1707,14 @@ class MethodTraceResolver( if (summary.isEmpty()) { if (unresolvedSkips.isEmpty()) { - return listOf(PartialCallEdgeCombination(unchanged, primary = null, mergedRules)) + return listOf( + PartialCallEdgeCombination(TraceEdges.conjoin(unchanged), primary = null, mergedRules) + ) } - val skippedEdges = unresolvedSkips.mapTo(hashSetOf()) { it.currentEdge } + val skippedEdges = TraceEdges.conjoin(unresolvedSkips.map { it.currentEdges }) val primary = MergedPrimaryUnresolvedCallSkip(UnresolvedCallSkip(skippedEdges, skippedEdges)) - return listOf(PartialCallEdgeCombination(unchanged, primary, mergedRules)) + return listOf(PartialCallEdgeCombination(TraceEdges.conjoin(unchanged), primary, mergedRules)) } if (unresolvedSkips.isNotEmpty()) { @@ -1230,14 +1722,10 @@ class MethodTraceResolver( return emptyList() } - val callees = resolveMethodCallees() - val result = mutableListOf() - callees.forEach { callee -> - methodEntryPoints(callee).forEach { - val primary = MergedPrimaryCall2StartAction(it, summary) - result += PartialCallEdgeCombination(unchanged, primary, mergedRules) - } + resolveCalleeEntryPoints().forEach { entryPoint -> + val primary = MergedPrimaryCall2StartAction(entryPoint, summary) + result += PartialCallEdgeCombination(TraceEdges.conjoin(unchanged), primary, mergedRules) } return result @@ -1246,33 +1734,37 @@ class MethodTraceResolver( private fun mergeCallRules(callRules: HashSet): Set { if (callRules.isEmpty()) return emptySet() - val sourceRules = hashMapOf>>() - val passRules = hashMapOf>>>() + val sourceRules = hashMapOf>() + val passRules = hashMapOf>>() for (unresolvedRule in callRules) { when (val rule = unresolvedRule.rule) { is TaintRulePrecondition.Pass -> passRules .getOrPut(rule.rule, ::hashMapOf) - .getOrPut(rule.condition, ::hashSetOf) - .addAll(rule.action.map { it to unresolvedRule.currentEdge }) + .getOrPut(rule.condition, ::mutableListOf) + .add(unresolvedRule) is TaintRulePrecondition.Source -> sourceRules - .getOrPut(rule.rule, ::hashSetOf) - .addAll(rule.action.map { it to unresolvedRule.currentEdge }) + .getOrPut(rule.rule, ::mutableListOf) + .add(unresolvedRule) } } val result = hashSetOf() - for ((rule, actionWithEdge) in sourceRules) { - val action = actionWithEdge.mapTo(hashSetOf()) { it.first } - val edges = actionWithEdge.mapTo(hashSetOf()) { it.second } + for ((rule, ruleActions) in sourceRules) { + val action = ruleActions.flatMapTo(hashSetOf()) { + (it.rule as TaintRulePrecondition.Source).action + } + val edges = TraceEdges.conjoin(ruleActions.map { it.currentEdges }) result += MergedRuleAction(edges, TaintRulePrecondition.Source(rule, action)) } for ((rule, conditionedActions) in passRules) { - for ((condition, actionWithEdge) in conditionedActions) { - val action = actionWithEdge.mapTo(hashSetOf()) { it.first } - val edges = actionWithEdge.mapTo(hashSetOf()) { it.second } + for ((condition, ruleActions) in conditionedActions) { + val action = ruleActions.flatMapTo(hashSetOf()) { + (it.rule as TaintRulePrecondition.Pass).action + } + val edges = TraceEdges.conjoin(ruleActions.map { it.currentEdges }) result += MergedRuleAction(edges, TaintRulePrecondition.Pass(rule, action, condition)) } } @@ -1282,17 +1774,17 @@ class MethodTraceResolver( private sealed interface PartiallyResolvedCallAction { data class CallRule( - val currentEdge: TraceEdge, + val currentEdges: TraceEdges, val rule: TaintRulePrecondition ) : PartiallyResolvedCallAction data class Call2Start( - val currentEdge: TraceEdge, + val currentEdges: TraceEdges, val call2Start: CallPreconditionFact.CallToStart, ): PartiallyResolvedCallAction data class UnresolvedCallSkip( - val currentEdge: TraceEdge, + val currentEdges: TraceEdges, ): PartiallyResolvedCallAction } @@ -1309,32 +1801,37 @@ class MethodTraceResolver( ) : PartiallyResolvedMergedPrimaryCallAction data class MergedRuleAction( - val currentEdges: Set, + val currentEdges: TraceEdges, val rule: TaintRulePrecondition ) : PartiallyResolvedMergedCallAction } private fun MutableList.propagateCall( - currentEdge: TraceEdge, + currentEdges: TraceEdges, preconditionFacts: List ) { for (fact in preconditionFacts) { when (fact) { is CallPreconditionFact.CallToReturnTaintRule -> { - if (fact.precondition is TaintRulePrecondition.Source && currentEdge !is TraceEdge.SourceTraceEdge) { + val ruleEdges = if (fact.precondition is TaintRulePrecondition.Source) { + TraceEdges.of(currentEdges.filterIsInstance()) + } else { + currentEdges + } + if (ruleEdges.isEmpty()) { // We search for pass-rule, not source rule continue } - this += PartiallyResolvedCallAction.CallRule(currentEdge, fact.precondition) + this += PartiallyResolvedCallAction.CallRule(ruleEdges, fact.precondition) } is CallPreconditionFact.CallToStart -> { - this += PartiallyResolvedCallAction.Call2Start(currentEdge, fact) + this += PartiallyResolvedCallAction.Call2Start(currentEdges, fact) } is CallPreconditionFact.UnresolvedCallSkip -> { - this += PartiallyResolvedCallAction.UnresolvedCallSkip(currentEdge) + this += PartiallyResolvedCallAction.UnresolvedCallSkip(currentEdges) } } } @@ -1359,7 +1856,8 @@ class MethodTraceResolver( null -> null is MergedPrimaryUnresolvedCallSkip -> listOf(primaryAction.action) is MergedPrimaryCall2StartAction -> { - resolveCallSummary(statement, primaryAction.calleeEntryPoint, primaryAction.call2Start) + val callee = primaryAction.calleeEntryPoint.overApproximateContext(primaryAction.call2Start) + resolveCallSummary(statement, callee, primaryAction.call2Start) } } @@ -1378,6 +1876,19 @@ class MethodTraceResolver( } } + private fun MethodEntryPoint.overApproximateContext( + call2Start: Set, + ): MethodEntryPoint { + val manager = analysisManager as? TaintAnalysisManager ?: return this + val contextIndependentFact = call2Start.all { action -> + action.currentEdges.all { it.fact.base == AccessPathBase.ClassStatic } + } + val method = manager.overApproximateMethodContext( + MethodWithContext(method, context), contextIndependentFact + ) + return MethodEntryPoint(method.ctx, statement) + } + private fun resolveCallSummary( statement: CommonInst, callee: MethodEntryPoint, @@ -1387,23 +1898,56 @@ class MethodTraceResolver( for (action in call2Start) { val edgeSummaries = mutableListOf() - val currentEdge = action.currentEdge - if (currentEdge is TraceEdge.SourceTraceEdge) { - edgeSummaries.resolveCallSourceSummary(currentEdge, callee, action.call2Start) - } + for (currentEdge in action.currentEdges) { + if (currentEdge is TraceEdge.SourceTraceEdge) { + edgeSummaries.resolveCallSourceSummary(currentEdge, callee, action.call2Start) + } - edgeSummaries.resolveCallPassSummary(currentEdge, callee, action.call2Start, statement) + edgeSummaries.resolveCallPassSummary(currentEdge, callee, action.call2Start, statement) + } if (edgeSummaries.isEmpty()) return emptyList() - resultSummaries.add(edgeSummaries) + resultSummaries.add(edgeSummaries.mergeEquivalentCallSummaries()) } - val resultActions = mutableListOf() + val resultActions = linkedSetOf() resultSummaries.forEachCartesianProduct { summaryGroup -> resultActions += mergeCallSummary(summaryGroup) ?: return@forEachCartesianProduct } - return resultActions + return resultActions.toList() + } + + private fun List.mergeEquivalentCallSummaries(): List = buildList { + this@mergeEquivalentCallSummaries.groupBy { it.summaryTrace }.values.forEach { equivalent -> + val edgeFacts = equivalent.mapNotNullTo(hashSetOf()) { + it.edges.premisesByFinalFact.keys.singleOrNull() + } + val edgeAfterFacts = equivalent.mapNotNullTo(hashSetOf()) { + it.edgesAfter.premisesByFinalFact.keys.singleOrNull() + } + val canMergeAsAlternatives = + edgeFacts.size == 1 && + edgeAfterFacts.size == 1 && + equivalent.all { + it.edges.premisesByFinalFact.size == 1 && + it.edgesAfter.premisesByFinalFact.size == 1 + } + + if (!canMergeAsAlternatives) { + addAll(equivalent) + return@forEach + } + + add( + CallSummary( + summaryEdges = equivalent.flatMapTo(hashSetOf()) { it.summaryEdges }, + summaryTrace = equivalent.first().summaryTrace, + edges = TraceEdges.of(equivalent.flatMap { it.edges }), + edgesAfter = TraceEdges.of(equivalent.flatMap { it.edgesAfter }), + ) + ) + } } private fun mergeCallSummary(callSummaries: Array): PrimaryAction? { @@ -1414,29 +1958,35 @@ class MethodTraceResolver( val exitStatement = callSummaries.first().summaryTrace.final.statement if (callSummaries.any { it.summaryTrace.final.statement != exitStatement }) return null - val finalEdges = hashSetOf() val summaryEdges = hashSetOf() for (summary in callSummaries) { summaryEdges += summary.summaryEdges - finalEdges += summary.summaryTrace.final.edges } - val summaryTraceFinal = TraceEntry.Final(finalEdges, exitStatement) + val summaryTraceFinal = TraceEntry.Final( + TraceEdges.conjoin(callSummaries.map { it.summaryTrace.final.edges }), + exitStatement, + ) val summaryTrace = SummaryTrace(callee, summaryTraceFinal, TraceKind.SummaryTrace) val sourceSummaryEdges = summaryEdges.filterIsInstanceTo(hashSetOf()) val summaryAction = if (sourceSummaryEdges.size == summaryEdges.size) { TraceEntryAction.CallSourceSummary(sourceSummaryEdges, summaryTrace) } else { - CallSummary(summaryEdges, summaryTrace) + CallSummary( + summaryEdges, + summaryTrace, + TraceEdges.conjoin(callSummaries.map { it.edges }), + TraceEdges.conjoin(callSummaries.map { it.edgesAfter }), + ) } return summaryAction } private fun resolveCallRule( - currentEdges: Set, + currentEdges: TraceEdges, rule: TaintRulePrecondition, preconditionFunction: MethodCallPrecondition, statement: CommonInst, @@ -1461,7 +2011,7 @@ class MethodTraceResolver( } private fun resolvePassCallRulePrecondition( - currentEdges: Set, + currentEdges: TraceEdges, statement: CommonInst, rule: TaintRulePrecondition.Pass, facts: List, @@ -1469,123 +2019,95 @@ class MethodTraceResolver( when (facts.size) { 0 -> error("impossible") 1 -> { - val initialFacts = currentEdges.flatMap { - when (it) { - is TraceEdge.SourceTraceEdge -> listOf(null) - is TraceEdge.MethodTraceEdge -> listOf(it.initialFact) - is TraceEdge.MethodTraceNDEdge -> it.initialFacts - } - }.distinct() - - if (initialFacts.size != 1) { - // unexpected different initial facts - return emptyList() - } - - val initialFact = initialFacts.first() - val edge = if (initialFact == null) { - TraceEdge.SourceTraceEdge(facts.first()) - } else { - TraceEdge.MethodTraceEdge(initialFact, facts.first()) - } - return listOf( - TraceEntryAction.CallRule(setOf(edge), currentEdges, rule.rule, rule.action) + TraceEntryAction.CallRule( + currentEdges.collapseToFact(facts.first()), + currentEdges, + rule.rule, + rule.action, + ) ) } else -> { - val result = mutableListOf() + val result = linkedSetOf() val allFactEdges = facts.map { resolveIntraProceduralTraceEdge(statement, it, includeStatement = false) } - val currentInitialFacts = object2IntMap() - - // note: we always have zero fact - val zeroFactIdx = addEdgeInitialFact(currentInitialFacts, fact = null) - currentEdges.forEach { addEdgeInitialFacts(currentInitialFacts, it) } - - val currentInitialFactsSet = BitSet(currentInitialFacts.size) - currentInitialFactsSet.set(0, currentInitialFacts.size) + val currentPremiseGroups = currentEdges.premisesByFinalFact.values.map { it.toList() } allFactEdges.cartesianProductMapTo { edgeGroup -> - var matchedInitials = BitSet(currentInitialFacts.size) - for (edge in edgeGroup) { - matchedInitials = addEdgeInitialFactsIfRegistered(currentInitialFacts, edge, matchedInitials) - ?: return@cartesianProductMapTo - } + currentPremiseGroups.forEachCartesianProduct { selectedCurrentPremises -> + if (!selectedCurrentPremises.asIterable().hasSameInitialFactsAs(edgeGroup.asIterable())) { + return@forEachCartesianProduct + } - // note: add zero fact since currentFactSet always contains it - matchedInitials.set(zeroFactIdx) - if (matchedInitials != currentInitialFactsSet) { - return@cartesianProductMapTo + result += TraceEntryAction.CallRule( + TraceEdges.of(edgeGroup.asIterable()), + TraceEdges.of(selectedCurrentPremises.asIterable()), + rule.rule, + rule.action, + ) } - - result += TraceEntryAction.CallRule(edgeGroup.toHashSet(), currentEdges, rule.rule, rule.action) } - return result + return result.toList() } } } - private fun addEdgeInitialFacts( - initialFactIndex: ConcurrentReadSafeObject2IntMap, - edge: TraceEdge, - ) = when (edge) { - is TraceEdge.SourceTraceEdge -> addEdgeInitialFact(initialFactIndex, fact = null) - is TraceEdge.MethodTraceEdge -> addEdgeInitialFact(initialFactIndex, edge.initialFact) - is TraceEdge.MethodTraceNDEdge -> edge.initialFacts.forEach { addEdgeInitialFact(initialFactIndex, it) } - } - - private fun addEdgeInitialFact( - initialFactIndex: ConcurrentReadSafeObject2IntMap, - fact: InitialFactAp?, - ): Int { - return initialFactIndex.getOrCreateIndex(fact?.replaceExclusions(ExclusionSet.Universe)) { return it } - } + private fun Iterable.hasSameInitialFactsAs(otherEdges: Iterable): Boolean = + flatMapTo(hashSetOf()) { it.normalizedInitialFacts() } == + otherEdges.flatMapTo(hashSetOf()) { it.normalizedInitialFacts() } - private fun addEdgeInitialFactsIfRegistered( - initialFactIndex: ConcurrentReadSafeObject2IntMap, - edge: TraceEdge, - factSet: BitSet, - ): BitSet? = when (edge) { - is TraceEdge.SourceTraceEdge -> addEdgeInitialFactIfRegistered(initialFactIndex, fact = null, factSet) - is TraceEdge.MethodTraceEdge -> addEdgeInitialFactIfRegistered(initialFactIndex, edge.initialFact, factSet) - is TraceEdge.MethodTraceNDEdge -> edge.initialFacts.fold(factSet as BitSet?) { acc, fact -> - acc?.let { addEdgeInitialFactIfRegistered(initialFactIndex, fact, it) } + private fun TraceEdge.normalizedInitialFacts(): Set = when (this) { + is TraceEdge.SourceTraceEdge -> emptySet() + is TraceEdge.MethodTraceEdge -> setOf(initialFact.replaceExclusions(ExclusionSet.Universe)) + is TraceEdge.MethodTraceNDEdge -> initialFacts.mapTo(hashSetOf()) { + it.replaceExclusions(ExclusionSet.Universe) } } - private fun addEdgeInitialFactIfRegistered( - initialFactIndex: ConcurrentReadSafeObject2IntMap, - fact: InitialFactAp?, - factSet: BitSet, - ): BitSet? { - val idx = initialFactIndex.getInt(fact?.replaceExclusions(ExclusionSet.Universe)) - if (idx == NO_VALUE) return null - factSet.set(idx) - return factSet - } - private fun MutableList.resolveCallPassSummary( currentEdge: TraceEdge, callee: MethodEntryPoint, startFact: CallPreconditionFact.CallToStart, statement: CommonInst ) { + addAll(cache.callPassSummaries(currentEdge, callee, startFact, statement) { + computeCallPassSummaries(currentEdge, callee, startFact, statement) + }) + } + + private fun computeCallPassSummaries( + currentEdge: TraceEdge, + callee: MethodEntryPoint, + startFact: CallPreconditionFact.CallToStart, + statement: CommonInst, + ): List { val resolvedCallSummaries = mutableListOf() - val methodSummaries = manager.findFactToFactSummaryEdges(callee, startFact.startFactBase) + val callerFact = startFact.callerFact + val finalFactPattern = (callerFact as? BaseOnlyInitialFactAp)?.let { + BaseOnlyFinalFactAp( + manager = it.manager, + base = startFact.startFactBase, + access = it.access, + exclusions = it.exclusions, + ) + } + val methodSummaries = if (finalFactPattern == null) { + manager.findFactToFactSummaryEdges(callee, startFact.startFactBase) + } else { + manager.findFactToFactSummaryEdges(callee, finalFactPattern) + } val applicableMethodSummaries = methodSummaries.filter { isApplicableExitToReturnEdge(it) } - val callerFact = startFact.callerFact for (summaryEdge in applicableMethodSummaries) { val mappedSummaryFact = summaryEdge.factAp.rebase(callerFact.base) val deltas = callerFact.splitDelta(mappedSummaryFact) - if (deltas.isEmpty()) continue // it is ok to map call arguments via exit2return @@ -1614,15 +2136,12 @@ class MethodTraceResolver( } } - val weakestCallSummaries = selectWeakestEntries(resolvedCallSummaries) - this += weakestCallSummaries - + val result = selectWeakestEntries(resolvedCallSummaries).toMutableList() val methodNdSummaries = manager.findFactNDSummaryEdges(callee, startFact.startFactBase) val applicableNDSummaries = methodNdSummaries.filter { isApplicableExitToReturnEdge(it) } for (summaryEdge in applicableNDSummaries) { val mappedSummaryFact = summaryEdge.factAp.rebase(callerFact.base) - if (!mappedSummaryFact.contains(callerFact)) continue val mappedSummaryInitialFacts = summaryEdge.initialFacts.map { @@ -1647,9 +2166,11 @@ class MethodTraceResolver( TraceSummaryEdge.MethodSummary(currentEdge.replaceFact(it), currentEdge, delta = null) } - this += CallSummary(callSummaries, calleeTrace) + result += CallSummary(callSummaries, calleeTrace) } } + + return result } private fun MutableList.resolveCallSourceSummary( @@ -1686,7 +2207,7 @@ class MethodTraceResolver( .groupBy { it.summaryTrace.final.statement } .values.forEach { entries -> val selectedEntries = LinkedList() - for (summary in entries) { + for (summary in entries.dropFieldEntriesCoveredByApplicableWildcard()) { addWeakestEntry(summary, selectedEntries) } result += selectedEntries @@ -1694,6 +2215,48 @@ class MethodTraceResolver( return result } + private fun List.dropFieldEntriesCoveredByApplicableWildcard(): List { + val wildcardEntries = filter { it.hasApplicableBaseOnlyWildcardSummary() } + if (wildcardEntries.isEmpty()) return this + return filterNot { entry -> + if (entry.hasApplicableBaseOnlyWildcardSummary()) return@filterNot false + wildcardEntries.any { wildcard -> entry.isCoveredByApplicableWildcard(wildcard) } + } + } + + private fun CallSummary.hasApplicableBaseOnlyWildcardSummary(): Boolean { + val summary = summaryEdges.singleOrNull() as? TraceSummaryEdge.MethodSummary ?: return false + val initial = summary.delta?.initialFact as? BaseOnlyInitialFactAp ?: return false + return initial.access == ABSTRACT_EMPTY_ACCESS + } + + private fun CallSummary.isCoveredByApplicableWildcard(wildcard: CallSummary): Boolean { + val summary = summaryEdges.singleOrNull() as? TraceSummaryEdge.MethodSummary ?: return false + val wildcardSummary = wildcard.summaryEdges.singleOrNull() as? TraceSummaryEdge.MethodSummary ?: return false + if (summary.edgeAfter != wildcardSummary.edgeAfter) return false + + val edge = summaryTrace.final.edges.singleOrNull() as? TraceEdge.MethodTraceEdge ?: return false + val wildcardEdge = wildcard.summaryTrace.final.edges.singleOrNull() as? TraceEdge.MethodTraceEdge ?: return false + if (summaryTrace.method != wildcard.summaryTrace.method) return false + if (summaryTrace.traceKind != wildcard.summaryTrace.traceKind) return false + if (summaryTrace.final.statement != wildcard.summaryTrace.final.statement) return false + if (edge.fact != wildcardEdge.fact) return false + + val initial = summary.delta?.initialFact as? BaseOnlyInitialFactAp ?: return false + val wildcardInitial = wildcardSummary.delta?.initialFact as? BaseOnlyInitialFactAp ?: return false + if (initial.projectFieldToWildcard() != wildcardInitial) return false + + val callerFact = summary.edge.fact as? BaseOnlyInitialFactAp ?: return false + val wildcardCallerFact = wildcardSummary.edge.fact as? BaseOnlyInitialFactAp ?: return false + if (callerFact.projectFieldToWildcard() != wildcardCallerFact) return false + return summary.edge.replaceFact(wildcardCallerFact) == wildcardSummary.edge + } + + private fun BaseOnlyInitialFactAp.projectFieldToWildcard(): BaseOnlyInitialFactAp? { + val generalizedAccess = access.eraseFieldForSummaryGeneralization() ?: return null + return BaseOnlyInitialFactAp(manager, base, generalizedAccess, exclusions) + } + private fun addWeakestEntry(entry: CallSummary, selectedEntries: LinkedList) { val entryFact = entry.edges.single().fact val iter = selectedEntries.listIterator() @@ -1747,10 +2310,12 @@ class MethodTraceResolver( private fun methodEntryPoints(method: MethodWithContext): Sequence = runner.graph.methodGraph(method.method).entryPoints().map { MethodEntryPoint(method.ctx, it) } - private fun containsEntryEdge(entryStatement: CommonInst, entryEdge: TraceEdge): Boolean { + private fun TraceBuilder.containsEntryEdge(entryStatement: CommonInst, entryEdge: TraceEdge): Boolean { when (entryEdge) { is TraceEdge.SourceTraceEdge -> { - val entryFacts = edges.allZeroToFactFactsAtStatement(entryStatement, entryEdge.fact) + val entryFacts = cache.zeroEntryFacts(entryStatement, entryEdge.fact.base) { + edges.allZeroToFactFactsAtStatement(entryStatement, entryEdge.fact) + } return entryFacts.any { statementFact -> statementFact.contains(entryEdge.fact) } } @@ -1766,6 +2331,13 @@ class MethodTraceResolver( } } + private fun TraceBuilder.containsEntryEdgeCached( + entryStatement: CommonInst, + entryEdge: TraceEdge, + ): Boolean = cache.containsEntryEdge(entryStatement, entryEdge) { + containsEntryEdge(entryStatement, entryEdge) + } + private fun TraceBuilder.debugTrace(): FullStart2FinalTrace { val successors = successors() val additionalSuccessors = Int2ObjectOpenHashMap() @@ -1810,23 +2382,5 @@ class MethodTraceResolver( private val logger = object : KLogging() {}.logger private const val TRACE_RESOLUTION_ACTION_HARD_LIMIT = 10_000 - private fun SummaryTrace.universeTrace() = - copy(final = final.run { copy(edges = edges.mapTo(hashSetOf()) { it.universeEdge() }) }) - - private fun TraceEdge.universeEdge() = when (this) { - is TraceEdge.SourceTraceEdge -> TraceEdge.SourceTraceEdge( - fact.replaceExclusions(ExclusionSet.Universe) - ) - - is TraceEdge.MethodTraceEdge -> TraceEdge.MethodTraceEdge( - initialFact.replaceExclusions(ExclusionSet.Universe), - fact.replaceExclusions(ExclusionSet.Universe) - ) - - is TraceEdge.MethodTraceNDEdge -> TraceEdge.MethodTraceNDEdge( - initialFacts.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) }, - fact.replaceExclusions(ExclusionSet.Universe) - ) - } } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/TraceResolver.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/TraceResolver.kt index b458e5c53..c62931ec4 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/TraceResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/TraceResolver.kt @@ -1,8 +1,18 @@ package org.opentaint.dataflow.ap.ifds.trace import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.TaintAnalysisUnitRunnerManager import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyInitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.NO_ACCESSOR +import org.opentaint.dataflow.ap.ifds.access.baseonly.fieldIdx +import org.opentaint.dataflow.ap.ifds.access.baseonly.rawSuffixSlot +import org.opentaint.dataflow.ap.ifds.access.baseonly.staticIdx +import org.opentaint.dataflow.ap.ifds.access.baseonly.suffixIdx +import org.opentaint.dataflow.ap.ifds.access.baseonly.valueAccessorState import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker.TaintVulnerability import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker.TaintVulnerabilityRuleNode @@ -14,18 +24,90 @@ import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.TraceResolutionResult. import org.opentaint.dataflow.util.Cancellation import org.opentaint.ir.api.common.CommonMethod import org.opentaint.ir.api.common.cfg.CommonInst +import java.util.PriorityQueue +import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeMark import kotlin.time.TimeSource +internal fun InitialFactAp.baseOnlyTraceFieldGeneralizationCovers(other: InitialFactAp): Boolean { + if (this == other) return true + val general = this as? BaseOnlyInitialFactAp ?: return false + val concrete = other as? BaseOnlyInitialFactAp ?: return false + if (general.base != concrete.base || general.exclusions != concrete.exclusions) return false + return general.access.staticIdx == concrete.access.staticIdx && + general.access.fieldIdx == NO_ACCESSOR && + concrete.access.fieldIdx != NO_ACCESSOR && + general.access.suffixIdx != NO_ACCESSOR && + general.access.suffixIdx == concrete.access.suffixIdx && + general.access.valueAccessorState == concrete.access.valueAccessorState +} + class TraceResolver( private val entryPointMethods: Set, private val manager: TaintAnalysisUnitRunnerManager, private val params: Params, private val cancellation: Cancellation ) { + private val start2FinalTraceCache = + ConcurrentHashMap() + private val generalizedStart2FinalTraceCache = + ConcurrentHashMap>() + private val methodEntryCallerTraceCache = + ConcurrentHashMap>>() + + private data class StartTraceCacheKey( + val method: MethodEntryPoint, + val statement: CommonInst, + val traceKind: MethodTraceResolver.TraceKind, + ) + + private data class CachedStartTrace( + val trace: MethodTraceResolver.SummaryTrace, + val result: ResolvedStartTraces, + ) + + private data class FieldGeneralizationCacheKey( + val start: StartTraceCacheKey, + val edges: Map, + ) + + private sealed interface FieldGeneralizationEdgeKey { + data class Source( + val fact: FieldGeneralizationFactKey, + ) : FieldGeneralizationEdgeKey + + data class Method( + val initial: FieldGeneralizationFactKey, + val final: FieldGeneralizationFactKey, + ) : FieldGeneralizationEdgeKey + + data class Exact( + val edge: MethodTraceResolver.TraceEdge, + ) : FieldGeneralizationEdgeKey + } + + private sealed interface FieldGeneralizationFactKey { + data class BaseOnly( + val base: AccessPathBase, + val staticIdx: Int, + val fieldIdx: Int, + val rawSuffixSlot: Int, + val exclusions: ExclusionSet, + ) : FieldGeneralizationFactKey + + data class Exact( + val fact: InitialFactAp, + ) : FieldGeneralizationFactKey + } + + private data class ResolvedStartTraces( + val traces: List, + ) + data class Params( val resolveEntryPointToStartTrace: Boolean = true, + val resolveAllTraces: Boolean = false, ) data class Trace( @@ -41,15 +123,22 @@ class TraceResolver( data class SourceToSinkTrace( val startNodes: Set, val sinkNodes: Set, - val successors: Map> + val successors: Map>, ) { fun findSuccessors( node: InterProceduralTraceNode, kind: CallKind, statement: CommonInst ) = successors[node]?.filter { it.kind == kind && it.statement == statement }.orEmpty() + fun findSuccessors(node: InterProceduralTraceNode, kind: CallKind) = + successors[node]?.filter { it.kind == kind }.orEmpty() + fun findSuccessors( node: InterProceduralTraceNode, kind: CallKind, statement: CommonInst, trace: MethodTraceResolver.SummaryTrace - ) = successors[node]?.filter { it.kind == kind && it.statement == statement && it.summary == trace }.orEmpty() + ) = successors[node]?.filter { + it.kind == kind && + it.statement == statement && + it.summary.withUniverseExclusions() == trace.withUniverseExclusions() + }.orEmpty() } sealed interface TraceNode { @@ -87,12 +176,19 @@ class TraceResolver( } data class InterProceduralSummaryTraceNode( - val trace: MethodTraceResolver.SummaryTrace + val trace: MethodTraceResolver.SummaryTrace, ) : InterProceduralTraceNode { override val methodEntryPoint: MethodEntryPoint get() = trace.method } + data class InterProceduralMethodEntryNode( + val entry: MethodEntry, + ) : InterProceduralTraceNode { + override val methodEntryPoint: MethodEntryPoint + get() = entry.entryPoint + } + // Enum can give non-determinacy as its entries have new hash code on every JVM run. // Override hashcode() and equals() when using enum as a field in classes whose objects // can be stored in sets etc. @@ -122,7 +218,10 @@ class TraceResolver( data class InProgress(val state: State) : TraceResolutionResult } - fun resolveTrace(state: State): TraceResolutionResult { + fun resolveTrace( + state: State, + isActive: () -> Boolean = cancellation::isActive, + ): TraceResolutionResult { when (state) { is State.Initial -> { val requests = mutableListOf() @@ -153,13 +252,19 @@ class TraceResolver( return NoTrace(state.vulnerability) } - val nextState = addNextRequest(state) + var nextState = addNextRequest(state) + if (params.resolveAllTraces) { + while (nextState.nextRequestIdx < state.requests.size) { + nextState = addNextRequest(nextState) + } + } + return TraceResolutionResult.InProgress(nextState) } ProcessingKind.PROCESS -> { val timeLimit = TimeSource.Monotonic.markNow() + 100.milliseconds - state.builder.process(stepLimit = 100, timeLimit) + state.builder.process(stepLimit = 100, timeLimit, isActive) if (!state.builder.isEmpty()) { return TraceResolutionResult.InProgress(state) @@ -203,7 +308,7 @@ class TraceResolver( val nextState = state.copy( nextRequestIdx = state.nextRequestIdx + 1, - kind = ProcessingKind.PROCESS + kind = ProcessingKind.PROCESS, ) return nextState } @@ -310,19 +415,37 @@ class TraceResolver( } } + private data class PrioritizedBuilderUnprocessedTrace( + val event: BuilderUnprocessedTrace, + val fieldSpecificity: Int, + ) + + private data class BuilderEventKey( + val trace: MethodTraceResolver.SummaryTrace, + val kind: CallKind, + val predecessor: InterProceduralCall?, + val successor: InterProceduralCall?, + ) + private inner class InterProceduralTraceGraphBuilder { val fullNodes = hashMapOf, InterProceduralTraceNode>>() val summaryNodes = hashMapOf, List>>() + val methodEntryNodes = hashMapOf() val sinkNodes = hashSetOf() val sourceNodes = hashSetOf() val rootNodes = hashSetOf() val successors = hashMapOf>() + private val seenEvents = hashSetOf() - val unprocessedCall2Source = mutableListOf() - val unprocessedCall2Sink = mutableListOf() + private val eventComparator = compareBy( + { it.fieldSpecificity }, + { -it.event.depth }, + ) + private val unprocessedCall2Source = PriorityQueue(eventComparator) + private val unprocessedCall2Sink = PriorityQueue(eventComparator) fun createSinkNode(trace: MethodTraceResolver.SummaryTrace) { val nodes = resolveNode(trace, CallKind.CallToSink, depth = 0) @@ -330,24 +453,34 @@ class TraceResolver( } private fun pollUnprocessedEvent(): BuilderUnprocessedTrace? { - unprocessedCall2Sink.removeLastOrNull()?.let { return it } - unprocessedCall2Source.removeLastOrNull()?.let { return it } + unprocessedCall2Sink.poll()?.let { return it.event } + unprocessedCall2Source.poll()?.let { return it.event } return null } private fun addUnprocessedEvent(event: BuilderUnprocessedTrace) { + val key = BuilderEventKey(event.trace, event.kind, event.predecessor, event.successor) + if (!seenEvents.add(key)) return + val prioritized = PrioritizedBuilderUnprocessedTrace( + event, + event.trace.fieldSpecificity(), + ) when (event.kind) { - CallKind.CallToSource -> unprocessedCall2Source.add(event) - CallKind.CallToSink -> unprocessedCall2Sink.add(event) + CallKind.CallToSource -> unprocessedCall2Source.add(prioritized) + CallKind.CallToSink -> unprocessedCall2Sink.add(prioritized) } } fun isEmpty(): Boolean = unprocessedCall2Sink.isEmpty() && unprocessedCall2Source.isEmpty() - fun process(stepLimit: Int, timeLimit: TimeMark) { + @Synchronized + fun process(stepLimit: Int, timeLimit: TimeMark, isActive: () -> Boolean) { var steps = 0 - while (cancellation.isActive() && ++steps < stepLimit && timeLimit.hasNotPassedNow()) { + while ( + cancellation.isActive() && isActive() && + ++steps < stepLimit && timeLimit.hasNotPassedNow() + ) { val event = pollUnprocessedEvent() ?: break val resolvedNodes = resolveNode(event.trace, event.kind, event.depth) @@ -366,62 +499,105 @@ class TraceResolver( } fun createSource2SinkTrace(): SourceToSinkTrace { - val rootsWithReachableSources = rootNodes.filter { node -> - entriesReachableFrom(successors, node, sourceNodes) { edge -> - edge.takeIf { it.kind == CallKind.CallToSource }?.node - } + val canReachSource = entriesThatCanReach(successors, sourceNodes) { edge -> + edge.takeIf { it.kind == CallKind.CallToSource }?.node } - - val rootsWithReachableSinks = rootsWithReachableSources.filterTo(hashSetOf()) { node -> - entriesReachableFrom(successors, node, sinkNodes) { edge -> - edge.takeIf { it.kind == CallKind.CallToSink }?.node - } + val canReachSink = entriesThatCanReach(successors, sinkNodes) { edge -> + edge.takeIf { it.kind == CallKind.CallToSink }?.node + } + val rootsWithReachableSinks = rootNodes.filterTo(hashSetOf()) { + it in canReachSource && it in canReachSink } if (rootsWithReachableSinks.isEmpty()) return SourceToSinkTrace(emptySet(), emptySet(), emptyMap()) - return SourceToSinkTrace(rootsWithReachableSinks, sinkNodes, successors) + return SourceToSinkTrace( + rootsWithReachableSinks, + sinkNodes, + successors, + ) } private fun resolveNode( trace: MethodTraceResolver.SummaryTrace, kind: CallKind, - depth: Int + depth: Int, ): List { - val traceNodes = summaryNodes.getOrPut(trace.method, ::hashMapOf) - val cacheKey = trace to kind + val normalizedTrace = trace.withUniverseExclusions() + val traceNodes = summaryNodes.getOrPut(normalizedTrace.method, ::hashMapOf) + val cacheKey = normalizedTrace to kind val currentNode = traceNodes[cacheKey] if (currentNode != null) return currentNode - val fullTraces = manager.withMethodRunner(trace.method) { - val traceResolver = methodTraceResolver(trace.method) - traceResolver.resolveIntraProceduralStart2FinalTrace(trace, cancellation) - } + val resolved = resolveStart2FinalTrace(normalizedTrace) val resultNodes = mutableListOf() + var retainedSummaryNode: InterProceduralSummaryTraceNode? = null - for (s2fTrace in fullTraces) { + for (s2fTrace in resolved.traces) { when (val start = s2fTrace.startEntry) { is SourceStartEntry -> { - resultNodes += resolveNode(s2fTrace, kind, depth) + val node = resolveNode(s2fTrace, kind, depth) + resultNodes += node } is MethodEntry -> { - check(kind != CallKind.CallToSource) { "Unexpected trace: $trace" } - - val node = InterProceduralStart2FinalTraceNode(s2fTrace) - resultNodes += node - - val callerTraces = resolveMethodEntry(start) - for ((callerStatement, callerTrace) in callerTraces) { - addUnprocessedEvent( - BuilderUnprocessedTrace( - trace = callerTrace, - kind = kind, - depth = depth + 1, - successor = InterProceduralCall(kind, callerStatement, trace, node) + check(kind != CallKind.CallToSource) { "Unexpected trace: $normalizedTrace" } + if (manager.apManager is BaseOnlyApManager) { + val summaryNode = retainedSummaryNode + ?: InterProceduralSummaryTraceNode(normalizedTrace).also { + retainedSummaryNode = it + resultNodes += it + } + val existingBoundary = methodEntryNodes[start] + val boundary = existingBoundary + ?: InterProceduralMethodEntryNode(start).also { + methodEntryNodes[start] = it + } + successors.getOrPut(boundary, ::hashSetOf).add( + InterProceduralCall( + kind, + normalizedTrace.final.statement, + normalizedTrace, + summaryNode, ) ) + if (existingBoundary == null) { + for ((callerStatement, callerTrace) in resolveMethodEntry(start)) { + addUnprocessedEvent( + BuilderUnprocessedTrace( + trace = callerTrace, + kind = kind, + depth = depth + 1, + successor = InterProceduralCall( + kind, + callerStatement, + normalizedTrace, + boundary, + ), + ) + ) + } + } + } else { + val node = InterProceduralStart2FinalTraceNode(s2fTrace) + val callerTraces = resolveMethodEntry(start) + for ((callerStatement, callerTrace) in callerTraces) { + addUnprocessedEvent( + BuilderUnprocessedTrace( + trace = callerTrace, + kind = kind, + depth = depth + 1, + successor = InterProceduralCall( + kind, + callerStatement, + normalizedTrace, + node, + ), + ) + ) + } + resultNodes += node } } } @@ -431,6 +607,135 @@ class TraceResolver( return resultNodes } + private fun resolveStart2FinalTrace( + trace: MethodTraceResolver.SummaryTrace, + ): ResolvedStartTraces = + start2FinalTraceCache.computeIfAbsent(trace) { + val cacheKey = trace.fieldGeneralizationCacheKey() + val generalized = generalizedStart2FinalTraceCache.computeIfAbsent(cacheKey) { mutableListOf() } + + synchronized(generalized) { + generalized.firstOrNull { it.trace.fieldGeneralizationCovers(trace) }?.let { + return@computeIfAbsent it.result + } + } + + val resolved = manager.withMethodRunner(trace.method) { + val traceResolver = methodTraceResolver(trace.method) + val traces = traceResolver.resolveIntraProceduralOverApproximateStart2FinalTrace( + trace, + cancellation, + ) + ResolvedStartTraces(traces) + } + + synchronized(generalized) { + generalized.firstOrNull { it.trace.fieldGeneralizationCovers(trace) }?.let { + return@computeIfAbsent it.result + } + if (resolved.traces.isNotEmpty()) { + generalized.removeIf { trace.fieldGeneralizationCovers(it.trace) } + generalized += CachedStartTrace(trace, resolved) + } + } + resolved + } + + private fun MethodTraceResolver.SummaryTrace.fieldGeneralizationCacheKey(): FieldGeneralizationCacheKey { + val start = StartTraceCacheKey(method, final.statement, traceKind) + val edges = final.edges + .groupingBy { it.fieldGeneralizationKey() } + .eachCount() + return FieldGeneralizationCacheKey(start, edges) + } + + private fun MethodTraceResolver.TraceEdge.fieldGeneralizationKey(): FieldGeneralizationEdgeKey = + when (this) { + is MethodTraceResolver.TraceEdge.SourceTraceEdge -> + FieldGeneralizationEdgeKey.Source(fact.fieldGeneralizationKey()) + + is MethodTraceResolver.TraceEdge.MethodTraceEdge -> + FieldGeneralizationEdgeKey.Method( + initialFact.fieldGeneralizationKey(), + fact.fieldGeneralizationKey(), + ) + + is MethodTraceResolver.TraceEdge.MethodTraceNDEdge -> + FieldGeneralizationEdgeKey.Exact(this) + } + + private fun InitialFactAp.fieldGeneralizationKey(): FieldGeneralizationFactKey { + val fact = this as? BaseOnlyInitialFactAp + ?: return FieldGeneralizationFactKey.Exact(this) + val projectedField = if (fact.access.suffixIdx == NO_ACCESSOR) { + fact.access.fieldIdx + } else { + NO_ACCESSOR + } + return FieldGeneralizationFactKey.BaseOnly( + fact.base, + fact.access.staticIdx, + projectedField, + fact.access.rawSuffixSlot, + fact.exclusions, + ) + } + + private fun MethodTraceResolver.SummaryTrace.fieldGeneralizationCovers( + other: MethodTraceResolver.SummaryTrace, + ): Boolean { + if (method != other.method || + traceKind != other.traceKind || + final.statement != other.final.statement || + final.edges.size != other.final.edges.size + ) { + return false + } + val available = final.edges.toMutableList() + for (otherEdge in other.final.edges) { + val coveringIdx = available.indexOfFirst { it.fieldGeneralizationCovers(otherEdge) } + if (coveringIdx < 0) return false + available.removeAt(coveringIdx) + } + return true + } + + private fun MethodTraceResolver.SummaryTrace.fieldSpecificity(): Int = + final.edges.sumOf { it.fieldSpecificity() } + + private fun MethodTraceResolver.TraceEdge.fieldSpecificity(): Int = when (this) { + is MethodTraceResolver.TraceEdge.SourceTraceEdge -> fact.fieldSpecificity() + is MethodTraceResolver.TraceEdge.MethodTraceEdge -> + initialFact.fieldSpecificity() + fact.fieldSpecificity() + + is MethodTraceResolver.TraceEdge.MethodTraceNDEdge -> + initialFacts.sumOf { it.fieldSpecificity() } + fact.fieldSpecificity() + } + + private fun InitialFactAp.fieldSpecificity(): Int { + val fact = this as? BaseOnlyInitialFactAp ?: return 0 + return if (fact.access.fieldIdx != NO_ACCESSOR && fact.access.suffixIdx != NO_ACCESSOR) 1 else 0 + } + + private fun MethodTraceResolver.TraceEdge.fieldGeneralizationCovers( + other: MethodTraceResolver.TraceEdge, + ): Boolean = when { + this is MethodTraceResolver.TraceEdge.SourceTraceEdge && + other is MethodTraceResolver.TraceEdge.SourceTraceEdge -> + fact.baseOnlyTraceFieldGeneralizationCovers(other.fact) + + this is MethodTraceResolver.TraceEdge.MethodTraceEdge && + other is MethodTraceResolver.TraceEdge.MethodTraceEdge -> + initialFact.baseOnlyTraceFieldGeneralizationCovers(other.initialFact) && + fact.baseOnlyTraceFieldGeneralizationCovers(other.fact) + + this is MethodTraceResolver.TraceEdge.MethodTraceNDEdge && + other is MethodTraceResolver.TraceEdge.MethodTraceNDEdge -> + this == other + + else -> false + } + private fun resolveNode(trace: MethodTraceResolver.Start2FinalTrace, kind: CallKind, depth: Int): InterProceduralTraceNode { val traceNodes = fullNodes.getOrPut(trace.method, ::hashMapOf) val cacheKey = trace to kind @@ -456,15 +761,16 @@ class TraceResolver( return node } + val normalizedSummary = callSummary.summaryTrace.withUniverseExclusions() addUnprocessedEvent( BuilderUnprocessedTrace( - trace = callSummary.summaryTrace, + trace = normalizedSummary, kind = CallKind.CallToSource, depth = depth + 1, predecessor = InterProceduralCall( CallKind.CallToSource, start.statement, - callSummary.summaryTrace, + normalizedSummary, node ) ) @@ -477,15 +783,19 @@ class TraceResolver( private fun resolveMethodEntry( methodEntry: MethodEntry - ): List> { - val callers = manager.findMethodCallers(methodEntry.entryPoint) - return callers.flatMap { caller -> - manager.withMethodRunner(caller.callerEp) { - val traceResolver = methodTraceResolver(caller.callerEp) - traceResolver.resolveIntraProceduralTraceFromCall(caller.statement, methodEntry) - }.map { caller.statement to it } + ): List> = + methodEntryCallerTraceCache.computeIfAbsent(methodEntry) { + val callers = manager.findMethodCallers( + methodEntry.entryPoint, + collectZeroCallsOnly = manager.apManager !is BaseOnlyApManager, + ) + callers.flatMap { caller -> + manager.withMethodRunner(caller.callerEp) { + val traceResolver = methodTraceResolver(caller.callerEp) + traceResolver.resolveIntraProceduralTraceFromCall(caller.statement, methodEntry) + }.map { caller.statement to it.withUniverseExclusions() } + }.distinct() } - } } inner class EntryPointToStartTraceBuilder { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/VulnerabilityWithTrace.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/VulnerabilityWithTrace.kt index 101e74cda..f144c7d32 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/VulnerabilityWithTrace.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/VulnerabilityWithTrace.kt @@ -5,7 +5,8 @@ import org.opentaint.dataflow.ap.ifds.trace.path.TracePathGenerationResult data class VulnerabilityWithInterproceduralTrace( val vulnerability: TaintSinkTracker.TaintVulnerability, - val trace: TraceResolver.Trace? + val trace: TraceResolver.Trace?, + val traceResolutionCompleted: Boolean = true, ) data class VulnerabilityWithTrace( diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceActionSearcher.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceActionSearcher.kt new file mode 100644 index 000000000..3954df1d6 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceActionSearcher.kt @@ -0,0 +1,690 @@ +package org.opentaint.dataflow.ap.ifds.trace.action + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import it.unimi.dsi.fastutil.ints.IntArrayList +import mu.KLogging +import org.opentaint.dataflow.ap.ifds.TaintAnalysisUnitRunnerManager +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.ActionVariant +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.FullStart2FinalTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.SummaryTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdge +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntry +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntryAction +import org.opentaint.dataflow.ap.ifds.trace.TraceResolver +import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithInterproceduralTrace +import org.opentaint.dataflow.ap.ifds.trace.path.Source2SinkTraceGraph +import org.opentaint.dataflow.ap.ifds.trace.path.createSource2SinkGraph +import org.opentaint.dataflow.ap.ifds.trace.withMethodRunner +import org.opentaint.dataflow.util.CompactIntSet +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSink +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSource +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.ir.api.common.cfg.CommonInst + +private val logger = object : KLogging() {}.logger + +private typealias Rules = Map>> + +private enum class RuleResolutionSkipReason { + UnchangedTaintMarks, + ZeroStartCoveredByPredecessor, +} + +sealed interface ActionableRulesCollectionResult { + data object Failed : ActionableRulesCollectionResult + data object Unprocessed : ActionableRulesCollectionResult + + data class Collected( + val rules: Map>>, + ) : ActionableRulesCollectionResult +} + +fun TaintAnalysisUnitRunnerManager.collectActionableRules( + vulnerability: VulnerabilityWithInterproceduralTrace, + operationCancellation: Cancellation = cancellation, +): ActionableRulesCollectionResult { + val trace = vulnerability.trace ?: return ActionableRulesCollectionResult.Failed + return collectActionableRules( + trace = trace, + sinkStatement = vulnerability.vulnerability.statement, + sinkRules = vulnerability.vulnerability.vulnerabilityRules.keys, + materializeNode = { node -> + withMethodRunner(node.methodEntryPoint) { + val resolver = methodTraceResolver(node.methodEntryPoint) + when (node) { + is TraceResolver.InterProceduralStart2FinalTraceNode -> + resolver.resolveIntraProceduralFullStart2FinalTrace( + node.trace, + operationCancellation, + collapseUnchangedNodes = true, + ) + + is TraceResolver.InterProceduralSummaryTraceNode -> + resolver.resolveIntraProceduralFullStart2FinalTrace( + node.trace, + operationCancellation, + collapseUnchangedNodes = true, + ) + + is TraceResolver.InterProceduralMethodEntryNode -> emptyList() + } + } + }, + materializeSummary = { summary -> + withMethodRunner(summary.method) { + methodTraceResolver(summary.method).resolveIntraProceduralFullStart2FinalTrace( + summary, + operationCancellation, + collapseUnchangedNodes = true, + ) + } + }, + isActive = operationCancellation::isActive, + ) +} + +fun collectActionableRules( + trace: TraceResolver.Trace, + sinkStatement: CommonInst, + sinkRules: Collection, + materializeNode: (TraceResolver.InterProceduralTraceNode) -> List, + materializeSummary: (SummaryTrace) -> List, + isActive: () -> Boolean = { true }, +): ActionableRulesCollectionResult = runCatching { + TraceActionCollector( + trace, + sinkStatement, + sinkRules, + materializeNode, + materializeSummary, + isActive, + ).collect() +}.getOrElse { + logger.error(it) { "Failed to collect actionable rules" } + ActionableRulesCollectionResult.Failed +} + +fun mergeActionableRules( + results: Iterable, +): Rules { + val merged = RulesAccumulator() + results.forEach { result -> merged.addAll(result.rules) } + return merged.freeze() +} + +internal fun SummaryTrace.shouldExpand(): Boolean { + val facts = final.edges.flatMapTo(linkedSetOf()) { it.boundaryFacts() } + if (facts.any { fact -> fact.getAllAccessors().any { it is TaintMarkAccessor } }) { + return true + } + return facts.any { !it.isAbstract() } +} + +private fun TraceEdge.boundaryFacts(): Set = when (this) { + is TraceEdge.SourceTraceEdge -> setOf(fact) + is TraceEdge.MethodTraceEdge -> setOf(initialFact, fact) + is TraceEdge.MethodTraceNDEdge -> initialFacts + fact +} + +internal fun Set.introducesOrChangesTaintMarks(): Boolean = + any { summaryEdge -> + when (summaryEdge) { + is TraceEntryAction.TraceSummaryEdge.SourceSummary -> true + is TraceEntryAction.TraceSummaryEdge.MethodSummary -> + summaryEdge.edge.fact.taintMarks() != summaryEdge.edgeAfter.fact.taintMarks() + } + } + +private fun InitialFactAp.taintMarks(): Set = + getAllAccessors().filterIsInstanceTo(linkedSetOf()) + +private class TraceActionCollector( + private val trace: TraceResolver.Trace, + private val sinkStatement: CommonInst, + sinkRules: Collection, + private val materializeNode: (TraceResolver.InterProceduralTraceNode) -> List, + private val materializeSummary: (SummaryTrace) -> List, + private val isActive: () -> Boolean, +) { + private enum class TraceOrigin { + OuterNode, + NestedSummary, + } + + private sealed interface Evaluation { + data class Valid(val rules: Rules) : Evaluation + data object Invalid : Evaluation + data object Failed : Evaluation + } + + private val sinkRules = sinkRules.toSet() + private val summaryResults = hashMapOf() + private val summariesInProgress = hashSetOf() + private val callSummaryRelevance = hashMapOf() + private val sharedNodeResults = hashMapOf() + private var unchangedTaintMarkNodes = 0 + private var coveredZeroStartNodes = 0 + private var sharedNodeResolutions = 0 + + fun collect(): ActionableRulesCollectionResult { + if (!isActive()) return ActionableRulesCollectionResult.Failed + check(sinkRules.isNotEmpty()) { "No sink rule attached to the vulnerability" } + check(sinkRules.all { it is CommonTaintConfigurationSink }) { + "Actionable-rule collection was seeded with a non-sink rule" + } + + val sourceToSink = trace.sourceToSinkTrace + val endpointNodes = sourceToSink.startNodes + sourceToSink.sinkNodes + if (endpointNodes.isNotEmpty() && endpointNodes.all { it is TraceResolver.SimpleTraceNode }) { + return ActionableRulesCollectionResult.Collected(sinkRuleMap()) + } + if (endpointNodes.any { it is TraceResolver.SimpleTraceNode }) { + return ActionableRulesCollectionResult.Failed + } + + val graph = createSource2SinkGraph(sourceToSink) + if (!isActive()) return ActionableRulesCollectionResult.Failed + val finalTaintMarks = graph.allNodes.map { sourceToSink.finalTaintMarks(it) } + + val nodeResults = arrayOfNulls(graph.allNodes.size) + for (nodeId in graph.allNodes.indices) { + if (!isActive()) return ActionableRulesCollectionResult.Failed + val seed = if (graph.sinkNodes.contains(nodeId)) sinkRuleMap() else emptyMap() + val result = when (graph.ruleResolutionSkipReason(nodeId, finalTaintMarks, sourceToSink)) { + RuleResolutionSkipReason.UnchangedTaintMarks -> { + unchangedTaintMarkNodes++ + Evaluation.Valid(seed) + } + + RuleResolutionSkipReason.ZeroStartCoveredByPredecessor -> { + coveredZeroStartNodes++ + evaluateZeroStartWithoutFullTrace(graph.allNodes[nodeId], seed) + } + + null -> evaluateNode(graph.allNodes[nodeId], seed) + } + if (result === Evaluation.Failed) return ActionableRulesCollectionResult.Failed + nodeResults[nodeId] = result + } + + val validNodes = graph.allNodes.indices + .filterTo(linkedSetOf()) { nodeResults[it] is Evaluation.Valid } + val reachableValidNodes = graph.corridor(validNodes, isActive) + ?: return ActionableRulesCollectionResult.Failed + + val collected = RulesAccumulator() + for (nodeId in reachableValidNodes) { + if (!isActive()) return ActionableRulesCollectionResult.Failed + val result = nodeResults[nodeId] as? Evaluation.Valid ?: continue + collected.addAll(result.rules) + } + + val rules = collected.freeze() + logger.debug { + "Rule search skipped $unchangedTaintMarkNodes unchanged-mark and " + + "$coveredZeroStartNodes covered-Zero " + + "full node resolutions out of ${graph.allNodes.size}; shared $sharedNodeResolutions " + + "identical full queries" + } + return if (rules.isEmpty()) { + ActionableRulesCollectionResult.Failed + } else { + ActionableRulesCollectionResult.Collected(rules) + } + } + + private fun evaluateNode( + node: TraceResolver.InterProceduralTraceNode, + seed: Rules, + ): Evaluation { + val sharedQuery = node.sharedFullTraceQuery() + if (sharedQuery != null) { + sharedNodeResults[sharedQuery]?.let { result -> + sharedNodeResolutions++ + return result.withSeed(seed) + } + } + + val traces = materializeNode(node) + if (traces.isEmpty()) { + if (sharedQuery != null) sharedNodeResults[sharedQuery] = Evaluation.Invalid + return Evaluation.Invalid + } + + if (!isActive()) return Evaluation.Failed + val result = evaluateResolvedTraces(traces, TraceOrigin.OuterNode, emptyMap()) + if (sharedQuery != null && result !== Evaluation.Failed) sharedNodeResults[sharedQuery] = result + return result.withSeed(seed) + } + + private fun TraceResolver.InterProceduralTraceNode.sharedFullTraceQuery(): SummaryTrace? = when (this) { + is TraceResolver.InterProceduralSummaryTraceNode -> null + is TraceResolver.InterProceduralMethodEntryNode -> null + is TraceResolver.InterProceduralStart2FinalTraceNode -> if (trace.isStartOverApproximation) { + SummaryTrace(trace.method, trace.final, trace.traceKind) + } else { + null + } + } + + private fun Evaluation.withSeed(seed: Rules): Evaluation { + if (this !is Evaluation.Valid || seed.isEmpty()) return this + val collected = RulesAccumulator() + collected.addAll(rules) + collected.addAll(seed) + return Evaluation.Valid(collected.freeze()) + } + + private fun evaluateZeroStartWithoutFullTrace( + node: TraceResolver.InterProceduralTraceNode, + seed: Rules, + ): Evaluation { + val startEntry = (node as TraceResolver.InterProceduralStart2FinalTraceNode) + .trace.startEntry as TraceEntry.SourceStartEntry + val collected = RulesAccumulator() + collected.addAll(seed) + collected.addRuleActions(startEntry) + return Evaluation.Valid(collected.freeze()) + } + + private fun evaluateSummary(summary: SummaryTrace): Evaluation { + summaryResults[summary]?.let { return it } + if (!summariesInProgress.add(summary)) return Evaluation.Invalid + + val traces = materializeSummary(summary) + if (traces.isEmpty()) return Evaluation.Invalid + + val evaluation = evaluateResolvedTraces(traces, TraceOrigin.NestedSummary, emptyMap()) + summariesInProgress.remove(summary) + + if (evaluation !== Evaluation.Failed) { + summaryResults[summary] = evaluation + } + return evaluation + } + + private fun evaluateResolvedTraces( + traces: List, + origin: TraceOrigin, + seed: Rules, + ): Evaluation { + if (traces.isEmpty()) return Evaluation.Invalid + + val collected = RulesAccumulator() + var hasValidTrace = false + for (fullTrace in traces) { + if (!isActive()) return Evaluation.Failed + when (val result = evaluateFullTrace(fullTrace, origin, seed)) { + is Evaluation.Valid -> { + hasValidTrace = true + collected.addAll(result.rules) + } + + Evaluation.Invalid -> Unit + Evaluation.Failed -> return Evaluation.Failed + } + } + + return if (hasValidTrace) Evaluation.Valid(collected.freeze()) else Evaluation.Invalid + } + + /** + * Evaluates one materialized intra-procedural trace. + * + * Relevant summaries are resolved first. An entry whose nested summary has + * no valid full trace is removed, then reachability is recomputed without + * all removed entries. Rules are projected only from the remaining + * start-to-final corridor. + */ + private fun evaluateFullTrace( + trace: FullStart2FinalTrace, + origin: TraceOrigin, + seed: Rules, + ): Evaluation { + val invalidEntries = hashSetOf() + val entryRules = hashMapOf() + + for ((entryId, entry) in trace.entries.withIndex()) { + if (!isActive()) return Evaluation.Failed + + if (entry is TraceEntry.Action) { + when (val result = evaluateActionVariants(trace, entry, entryId)) { + is Evaluation.Valid -> entryRules[entryId] = result.rules + Evaluation.Invalid -> invalidEntries += entryId + Evaluation.Failed -> return Evaluation.Failed + } + continue + } + + val summary = entry.relevantSummary(origin) ?: continue + when (val nestedResult = evaluateSummary(summary)) { + is Evaluation.Valid -> entryRules[entryId] = nestedResult.rules + Evaluation.Invalid -> invalidEntries += entryId + Evaluation.Failed -> return Evaluation.Failed + } + } + + val collected = RulesAccumulator() + collected.addAll(seed) + + fun collectEntry(entryId: Int) { + entryRules[entryId]?.let(collected::addAll) + val entry = trace.entries[entryId] + if (entry !is TraceEntry.Action) { + collected.addRuleActions(entry) + } + } + + if (invalidEntries.isEmpty()) { + trace.entries.indices.forEach { entryId -> + if (!isActive()) return Evaluation.Failed + collectEntry(entryId) + } + return Evaluation.Valid(collected.freeze()) + } + + val reachableEntries = trace.corridorWithout(invalidEntries, isActive) + if (!reachableEntries.contains(trace.finalId)) return Evaluation.Invalid + reachableEntries.forEach { entryId -> + if (!isActive()) return Evaluation.Failed + collectEntry(entryId) + } + + return Evaluation.Valid(collected.freeze()) + } + + private fun evaluateActionVariants( + trace: FullStart2FinalTrace, + entry: TraceEntry.Action, + entryId: Int, + ): Evaluation { + val variants = trace.actionVariants.get(entryId) + + var hasValidVariant = false + val collected = RulesAccumulator() + for (variant in variants) { + if (!isActive()) return Evaluation.Failed + + val summary = variant.relevantSummary() + if (summary != null) { + when (val nestedResult = evaluateSummary(summary)) { + is Evaluation.Valid -> collected.addAll(nestedResult.rules) + Evaluation.Invalid -> continue + Evaluation.Failed -> return Evaluation.Failed + } + } + + hasValidVariant = true + collected.addRuleActions(entry.statement, variant.otherActions) + } + + return if (hasValidVariant) { + Evaluation.Valid(collected.freeze()) + } else { + Evaluation.Invalid + } + } + + private fun ActionVariant.relevantSummary(): SummaryTrace? = + when (val action = primaryAction) { + is TraceEntryAction.CallSourceSummary -> action.summaryTrace + is TraceEntryAction.CallSummary -> action.summaryTrace.takeIf { summary -> + callSummaryRelevance.getOrPut(action) { + action.summaryEdges.introducesOrChangesTaintMarks() && summary.shouldExpand() + } + } + else -> null + } + + private fun TraceEntry.relevantSummary(origin: TraceOrigin): SummaryTrace? = when (this) { + is TraceEntry.SourceStartEntry -> { + val action = sourcePrimaryAction + if (origin == TraceOrigin.NestedSummary && action is TraceEntryAction.CallSourceSummary) { + action.summaryTrace + } else { + null + } + } + + else -> null + } + + private fun RulesAccumulator.addRuleActions(entry: TraceEntry) { + val actions: Iterable = when (entry) { + is TraceEntry.SourceStartEntry -> entry.sourceOtherActions + else -> emptyList() + } + addRuleActions(entry.statement, actions) + } + + private fun RulesAccumulator.addRuleActions( + statement: CommonInst, + actions: Iterable, + ) { + actions.forEach { action -> + when (action) { + is TraceEntryAction.CallRuleAction -> { + if (action.rule is CommonTaintConfigurationSource) { + addAction(statement, action.rule, action.action) + } + } + + is TraceEntryAction.SequentialSourceRule -> { + addAction(statement, action.rule, action.action) + } + + is TraceEntryAction.CallSourceSummary, + is TraceEntryAction.CallSummary, + is TraceEntryAction.UnresolvedCallSkip, + is TraceEntryAction.Sequential -> { + // skip, no rules + } + } + } + } + + private fun sinkRuleMap(): Rules { + val rules = RulesAccumulator() + sinkRules.forEach { rules.addSink(sinkStatement, it) } + return rules.freeze() + } +} + +private fun Source2SinkTraceGraph.ruleResolutionSkipReason( + nodeId: Int, + finalTaintMarks: List>, + sourceToSink: TraceResolver.SourceToSinkTrace, +): RuleResolutionSkipReason? { + val node = allNodes[nodeId] + val finalMarks = finalTaintMarks[nodeId] + + if (node is TraceResolver.InterProceduralMethodEntryNode) { + return RuleResolutionSkipReason.UnchangedTaintMarks + } + + val trace = (node as? TraceResolver.InterProceduralStart2FinalTraceNode)?.trace ?: return null + return when (val startEntry = trace.startEntry) { + is TraceEntry.MethodEntry -> RuleResolutionSkipReason.UnchangedTaintMarks.takeIf { + startEntry.facts.taintMarks() == finalMarks + } + + is TraceEntry.SourceStartEntry -> RuleResolutionSkipReason.ZeroStartCoveredByPredecessor.takeIf { + directPredecessors(nodeId).any { predecessorId -> + finalTaintMarks[predecessorId] == finalMarks + } + } + } +} + +private fun Source2SinkTraceGraph.directPredecessors(nodeId: Int): Set = buildSet { + root2SourceBwd[nodeId]?.forEach { add(it) } + root2SinkBwd[nodeId]?.forEach { add(it) } +} + +private fun TraceResolver.SourceToSinkTrace.finalTaintMarks( + node: TraceResolver.InterProceduralTraceNode, +): Set = + when (node) { + is TraceResolver.InterProceduralStart2FinalTraceNode -> node.trace.final.taintMarks() + is TraceResolver.InterProceduralSummaryTraceNode -> node.trace.final.taintMarks() + is TraceResolver.InterProceduralMethodEntryNode -> node.entry.facts.taintMarks() + } + +private fun TraceEntry.Final.taintMarks(): Set = + edges.mapToTaintMarks { it.fact } + +private fun Set.taintMarks(): Set = + mapToTaintMarks { it } + +private inline fun Iterable.mapToTaintMarks( + fact: (T) -> InitialFactAp, +): Set = buildSet { + for (element in this@mapToTaintMarks) { + addAll(fact(element).taintMarks()) + } +} + +private class RulesAccumulator { + private val rules = + linkedMapOf>>() + + fun addSink(statement: CommonInst, rule: CommonTaintConfigurationItem) { + check(rule is CommonTaintConfigurationSink) { "Non-sink rule has an empty action set: $rule" } + val statementRules = rules.getOrPut(statement) { linkedMapOf() } + check(statementRules[rule]?.isNotEmpty() != true) { + "Configuration item is both a sink and an action-owning rule: $rule" + } + statementRules.getOrPut(rule) { linkedSetOf() } + } + + fun addAction( + statement: CommonInst, + rule: CommonTaintConfigurationItem, + actions: Set, + ) { + check(actions.isNotEmpty()) { "Rule action has an empty action set: $rule" } + check(rule !is CommonTaintConfigurationSink) { "Sink rule has actions: $rule" } + rules.getOrPut(statement) { linkedMapOf() } + .getOrPut(rule) { linkedSetOf() } + .addAll(actions) + } + + fun addAll(other: Rules) { + other.forEach { (statement, statementRules) -> + statementRules.forEach { (rule, actions) -> + if (actions.isEmpty()) addSink(statement, rule) else addAction(statement, rule, actions) + } + } + } + + fun freeze(): Rules = rules.mapValues { (_, statementRules) -> + statementRules.mapValues { (_, actions) -> actions.toSet() }.toMap() + }.toMap() +} + +private fun FullStart2FinalTrace.corridorWithout( + invalidEntries: Set, + isActive: () -> Boolean, +): CompactIntSet { + fun isAllowed(entryId: Int): Boolean = + entryId in entries.indices && entryId !in invalidEntries + + if (!isAllowed(startEntryId) || !isAllowed(finalId) || !isActive()) return CompactIntSet() + + val reachable = reachableCompactNodes(setOf(startEntryId), ::isAllowed, isActive, successors::get) + + val predecessors = Int2ObjectOpenHashMap() + reachable.forEach { from -> + if (!isActive()) return CompactIntSet() + successors.get(from)?.forEach { to -> + if (reachable.contains(to)) { + predecessors.computeIfAbsent(to) { CompactIntSet() }.add(from) + } + } + } + return reachableCompactNodes(setOf(finalId), reachable::contains, isActive, predecessors::get) +} + +private fun Source2SinkTraceGraph.corridor( + allowedNodes: Set, + isActive: () -> Boolean, +): Set? { + if (allowedNodes.isEmpty() || !isActive()) return null + + val sources = sourceNodes.toIntArray().filterTo(linkedSetOf()) { isActive() && it in allowedNodes } + val sinks = sinkNodes.toIntArray().filterTo(linkedSetOf()) { isActive() && it in allowedNodes } + val roots = rootNodes.toIntArray().filterTo(linkedSetOf()) { isActive() && it in allowedNodes } + if (sources.isEmpty() || sinks.isEmpty() || roots.isEmpty()) return null + + val canReachSource = reachableNodes(sources, allowedNodes, isActive) { + root2SourceBwd[it]?.toIntArray()?.asList().orEmpty() + } + val canReachSink = reachableNodes(sinks, allowedNodes, isActive) { + root2SinkBwd[it]?.toIntArray()?.asList().orEmpty() + } + if (!isActive()) return null + val completeRoots = roots.filterTo(linkedSetOf()) { + isActive() && it in canReachSource && it in canReachSink + } + if (completeRoots.isEmpty()) return null + + val sourceForward = reachableNodes(completeRoots, allowedNodes, isActive) { + root2SourceFwd[it]?.toIntArray()?.asList().orEmpty() + } + val sinkForward = reachableNodes(completeRoots, allowedNodes, isActive) { + root2SinkFwd[it]?.toIntArray()?.asList().orEmpty() + } + if (!isActive()) return null + val sourceCorridor = sourceForward.intersect(canReachSource) + val sinkCorridor = sinkForward.intersect(canReachSink) + return (sourceCorridor + sinkCorridor).takeIf { it.isNotEmpty() } +} + +private fun reachableNodes( + initial: Collection, + allowed: Set, + isActive: () -> Boolean, + next: (Int) -> Iterable, +): Set { + val reached = linkedSetOf() + val pending = ArrayDeque() + initial.filterTo(pending) { isActive() && it in allowed } + while (pending.isNotEmpty()) { + if (!isActive()) return emptySet() + val node = pending.removeFirst() + if (!reached.add(node)) continue + for (successor in next(node)) { + if (!isActive()) return emptySet() + if (successor in allowed && successor !in reached) pending.addLast(successor) + } + } + return reached +} + +private fun reachableCompactNodes( + initial: Collection, + isAllowed: (Int) -> Boolean, + isActive: () -> Boolean, + next: (Int) -> CompactIntSet?, +): CompactIntSet { + val reached = CompactIntSet() + val pending = IntArrayList() + initial.forEach { + if (isActive() && isAllowed(it)) pending.add(it) + } + while (pending.isNotEmpty()) { + if (!isActive()) return CompactIntSet() + val node = pending.removeInt(pending.lastIndex) + if (reached.contains(node)) continue + reached.add(node) + next(node)?.forEach { successor -> + if (!isActive()) return CompactIntSet() + if (isAllowed(successor) && !reached.contains(successor)) pending.add(successor) + } + } + return reached +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/Source2SinkTraceGraph.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/Source2SinkTraceGraph.kt index a401180dc..84c018578 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/Source2SinkTraceGraph.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/Source2SinkTraceGraph.kt @@ -10,6 +10,7 @@ import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntryAction import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.CallKind.CallToSink import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.CallKind.CallToSource import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.InterProceduralStart2FinalTraceNode +import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.InterProceduralMethodEntryNode import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.InterProceduralSummaryTraceNode import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.InterProceduralTraceNode import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.SourceToSinkTrace @@ -113,13 +114,15 @@ private fun Source2SinkTraceGraph.traverseStartToSink( return } - val finalEntry = when (node) { - is InterProceduralStart2FinalTraceNode -> node.trace.final - is InterProceduralSummaryTraceNode -> node.trace.final - } + val sinkSuccessors = when (node) { + is InterProceduralStart2FinalTraceNode -> + trace.findSuccessors(node, kind = CallToSink, node.trace.final.statement) + + is InterProceduralSummaryTraceNode -> + trace.findSuccessors(node, kind = CallToSink, node.trace.final.statement) - val lastStatement = finalEntry.statement - val sinkSuccessors = trace.findSuccessors(node, kind = CallToSink, lastStatement) + is InterProceduralMethodEntryNode -> trace.findSuccessors(node, kind = CallToSink) + } if (sinkSuccessors.isEmpty()) { // todo: fix trace return @@ -146,16 +149,31 @@ private object NodeComparator : Comparator { ): Int = when (a) { is InterProceduralSummaryTraceNode -> when (b) { is InterProceduralSummaryTraceNode -> SummaryNodeComparator.compare(a, b) + is InterProceduralMethodEntryNode, is InterProceduralStart2FinalTraceNode -> -1 } - is InterProceduralStart2FinalTraceNode -> when (b) { + is InterProceduralMethodEntryNode -> when (b) { is InterProceduralSummaryTraceNode -> 1 + is InterProceduralMethodEntryNode -> MethodEntryNodeComparator.compare(a, b) + is InterProceduralStart2FinalTraceNode -> -1 + } + + is InterProceduralStart2FinalTraceNode -> when (b) { + is InterProceduralSummaryTraceNode, + is InterProceduralMethodEntryNode -> 1 is InterProceduralStart2FinalTraceNode -> FullNodeComparator.compare(a, b) } } } +private object MethodEntryNodeComparator : Comparator { + override fun compare(a: InterProceduralMethodEntryNode, b: InterProceduralMethodEntryNode): Int { + MethodComparator.compare(a.entry.entryPoint, b.entry.entryPoint).let { if (it != 0) return it } + return a.entry.facts.hashCode().compareTo(b.entry.facts.hashCode()) + } +} + private object SummaryNodeComparator : Comparator { override fun compare( a: InterProceduralSummaryTraceNode, diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/TracePath.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/TracePath.kt index 766ab66ed..2cb7c911c 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/TracePath.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/path/TracePath.kt @@ -1,5 +1,6 @@ package org.opentaint.dataflow.ap.ifds.trace.path +import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntObjectImmutablePair import it.unimi.dsi.fastutil.ints.IntOpenHashSet import mu.KLogging @@ -63,7 +64,12 @@ fun TaintAnalysisUnitRunnerManager.generateTracePath( } } -private class NodeTrace(val sink2Root: IntArray, val root2Source: IntArray) +internal class NodeTrace(val sink2Root: IntArray, val root2Source: IntArray) + +internal data class NodesForPathResolution( + val root2Source: List, + val root2SinkNoRoot: List, +) sealed interface ResolvedInterProceduralTraceEntry { val entry: TraceEntry @@ -116,21 +122,34 @@ private fun Source2SinkTraceGraph.resolvedNodeTrace( runner: TaintAnalysisUnitRunnerManager, params: TracePathResolveParams, ): ResolvedNodeTrace? { - val root2Source = trace.root2Source.map { allNodes[it] } - val root2Sink = trace.sink2Root.map { allNodes[it] }.reversed() + val nodes = nodesForPathResolution(trace.sink2Root, trace.root2Source) - val resolvedRoot2Source = root2Source.map { + val resolvedRoot2Source = nodes.root2Source.map { runner.resolveNodePath(it, params) ?: return null } - val rootToSinkNoRoot = root2Sink.drop(1).map { + val rootToSinkNoRoot = nodes.root2SinkNoRoot.map { runner.resolveNodePath(it, params) ?: return null } return ResolvedNodeTrace(resolvedRoot2Source, rootToSinkNoRoot) } -private fun Source2SinkTraceGraph.processMethodTrace( +internal fun Source2SinkTraceGraph.nodesForPathResolution( + sink2Root: IntArray, + root2Source: IntArray, +): NodesForPathResolution = NodesForPathResolution( + root2Source = root2Source + .map { allNodes[it] } + .filterNot { it is TraceResolver.InterProceduralMethodEntryNode }, + root2SinkNoRoot = sink2Root + .map { allNodes[it] } + .asReversed() + .drop(1) + .filterNot { it is TraceResolver.InterProceduralMethodEntryNode }, +) + +internal fun Source2SinkTraceGraph.processMethodTrace( mg: Source2SinkMethodTraceGraph, trace: MethodTrace, handleNodeTrace: (NodeTrace) -> T? @@ -138,8 +157,9 @@ private fun Source2SinkTraceGraph.processMethodTrace( val result = NodeTrace(IntArray(trace.sink2Root.size), IntArray(trace.root2Source.size)) val sinkMethod = trace.sink2Root[0] mg.sink2RootMethodNodes.get(sinkMethod)?.forEachInt { node -> + if (allNodes[node] is TraceResolver.InterProceduralMethodEntryNode) return@forEachInt result.sink2Root[0] = node - processMethodTrace( + processMethodTraceNodes( 1, trace.sink2Root, result.sink2Root, @@ -147,7 +167,7 @@ private fun Source2SinkTraceGraph.processMethodTrace( { root2SinkBwd.get(it) } ) { result.root2Source[0] = result.sink2Root.last() - processMethodTrace( + processMethodTraceNodes( 1, trace.root2Source, result.root2Source, @@ -161,7 +181,7 @@ private fun Source2SinkTraceGraph.processMethodTrace( return null } -private fun processMethodTrace( +private fun Source2SinkTraceGraph.processMethodTraceNodes( i: Int, traceArray: IntArray, nodeTraceArray: IntArray, @@ -179,20 +199,41 @@ private fun processMethodTrace( val curCandidateNodes = methodNodes(curMethodId) ?: return null - val successorNodes = nodeSuccessors(prevNode) - ?: return null + val successorNodes = successorsAcrossMethodEntryBoundaries(prevNode, nodeSuccessors) successorNodes.forEachInt { succNode -> if (!curCandidateNodes.contains(succNode)) return@forEachInt nodeTraceArray[i] = succNode - processMethodTrace(i + 1, traceArray, nodeTraceArray, methodNodes, nodeSuccessors, next) + processMethodTraceNodes(i + 1, traceArray, nodeTraceArray, methodNodes, nodeSuccessors, next) ?.let { return it } } return null } +private fun Source2SinkTraceGraph.successorsAcrossMethodEntryBoundaries( + node: Int, + nodeSuccessors: (Int) -> IntOpenHashSet?, +): IntOpenHashSet { + val result = IntOpenHashSet() + val visitedBoundaries = IntOpenHashSet() + val pending = IntArrayList() + nodeSuccessors(node)?.forEachInt { pending.add(it) } + + while (pending.size > 0) { + val successor = pending.removeInt(pending.size - 1) + if (allNodes[successor] !is TraceResolver.InterProceduralMethodEntryNode) { + result.add(successor) + continue + } + if (!visitedBoundaries.add(successor)) continue + nodeSuccessors(successor)?.forEachInt { pending.add(it) } + } + + return result +} + private fun TaintAnalysisUnitRunnerManager.resolveNodePath( node: TraceResolver.InterProceduralTraceNode, params: TracePathResolveParams, @@ -211,6 +252,8 @@ private fun TaintAnalysisUnitRunnerManager.resolveNodePath( node.trace, cancellation, collapseUnchangedNodes = true ) } + + is TraceResolver.InterProceduralMethodEntryNode -> emptyList() } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Cleaner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Cleaner.kt index 241c68ae5..9009acf8e 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Cleaner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Cleaner.kt @@ -20,7 +20,7 @@ class TaintCleanActionEvaluator { if (from is PositionAccess.Simple) { val actionInfo = EvaluatedCleanAction.ActionInfo(rule, action) - return listOf(EvaluatedCleanAction(fact = null, actionInfo, evc)) + return listOf(EvaluatedCleanAction(fact = null, actionInfo)) } val cleanAccessors = from.accessorList() @@ -54,13 +54,13 @@ class TaintCleanActionEvaluator { val result = mutableListOf() if (factCleaned) { val actionInfo = EvaluatedCleanAction.ActionInfo(rule, action) - result += EvaluatedCleanAction(null, actionInfo, evc) + result += EvaluatedCleanAction(null, actionInfo) } return cleanedFacts.mapTo(result) { cleanedFact -> val resultFact = fact.replaceFact(cleanedFact) val actionInfo = EvaluatedCleanAction.ActionInfo(rule, action) - EvaluatedCleanAction(resultFact, actionInfo, evc) + EvaluatedCleanAction(resultFact, actionInfo) } } @@ -80,7 +80,7 @@ class TaintCleanActionEvaluator { val cleaned = clearedAfterAny != factAfterAny || cleanedWithoutAny != factWithoutAny - return listOfNotNull(restoredAfterAny, cleanedWithoutAny) to cleaned + return listOfNotNull(restoredAfterAny, cleanedWithoutAny).distinct() to cleaned } if (!fact.startsWithAccessor(head)) { @@ -99,7 +99,7 @@ class TaintCleanActionEvaluator { val remaining = listOfNotNull(fact.clearAccessor(head)) val (cleanChild, childCleaned) = clearPosition(tail, child) val cleanChildWithAccessor = cleanChild.map { it.prependAccessor(head) } - val fullFact = remaining + cleanChildWithAccessor + val fullFact = (remaining + cleanChildWithAccessor).distinct() return fullFact to childCleaned } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/EvaluatedCleanAction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/EvaluatedCleanAction.kt index abff1ae97..21c0ef147 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/EvaluatedCleanAction.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/EvaluatedCleanAction.kt @@ -6,7 +6,6 @@ import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem data class EvaluatedCleanAction( val fact: FinalFactReader?, val action: ActionInfo?, - val prev: EvaluatedCleanAction?, ) { data class ActionInfo( val rule: CommonTaintConfigurationItem, @@ -15,7 +14,7 @@ data class EvaluatedCleanAction( companion object { fun initial(fact: FinalFactReader) = EvaluatedCleanAction( - action = null, fact = fact, prev = null + action = null, fact = fact ) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReader.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReader.kt index 1f738a5a1..a03c62da9 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReader.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReader.kt @@ -51,6 +51,8 @@ class FinalFactReader( fun replaceFact(factAp: FinalFactAp) = FinalFactReader(factAp, apManager).also { it.refinement = refinement } + fun copy() = FinalFactReader(factAp, apManager).also { it.refinement = refinement } + fun refineFact(factAp: InitialFactAp): InitialFactAp { if (!hasRefinement) return factAp val refinedAp = factAp.replaceExclusions(factAp.exclusions.union(refinement)) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/Cancellation.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/Cancellation.kt index 6f633b920..e95f2dea5 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/Cancellation.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/Cancellation.kt @@ -1,7 +1,14 @@ package org.opentaint.dataflow.util -class Cancellation { - class Cancelled : Exception("Operation cancelled") { +import java.util.concurrent.CancellationException + +class Cancellation private constructor( + private val parent: Cancellation?, + private val additionalCondition: (() -> Boolean)?, +) { + constructor() : this(parent = null, additionalCondition = null) + + class Cancelled : CancellationException("Operation cancelled") { override fun fillInStackTrace(): Throwable = this } @@ -16,10 +23,14 @@ class Cancellation { isActive = false } - fun isActive(): Boolean = isActive + fun isActive(): Boolean = + isActive && parent?.isActive() != false && additionalCondition?.invoke() != false + + fun derive(additionalCondition: () -> Boolean): Cancellation = + Cancellation(parent = this, additionalCondition = additionalCondition) fun checkpoint() { - if (isActive) return + if (isActive()) return throw Cancelled() } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MapUtils.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MapUtils.kt index 6af0e3a38..9fe032b99 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MapUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MapUtils.kt @@ -5,6 +5,10 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap fun int2ObjectMap() = ConcurrentReadSafeInt2ObjectMap() +fun long2ObjectMap() = ConcurrentReadSafeLong2ObjectMap() + +fun longSet() = ConcurrentReadSafeLongSet() + inline fun ConcurrentReadSafeInt2ObjectMap.forEachEntry(body: (Int, V) -> Unit) { if (isEmpty()) return @@ -32,6 +36,57 @@ inline fun ConcurrentReadSafeInt2ObjectMap.forEachEntry(body: (Int, V) -> } } +inline fun ConcurrentReadSafeLong2ObjectMap.forEachEntry(body: (Long, V) -> Unit) { + if (isEmpty()) return + + while (true) { + val containsNullKey = getContainsNullKey() + val key = getKeys() + val value = getValues() + val n = getN() + + // Capture arrays from one table generation to allow a read during rehash. + if (key.size != n + 1 || value.size != n + 1) continue + + if (containsNullKey) { + // A writer publishes the key before the value. A concurrent reader may briefly see null. + value[n]?.let { body(0, it) } + } + + for (i in 0 until n) { + val k = key[i] + if (k == 0L) continue + + // Weak iteration may omit an entry being published, but must never expose a null value. + value[i]?.let { body(k, it) } + } + + return + } +} + +inline fun ConcurrentReadSafeLongSet.forEachLong(body: (Long) -> Unit) { + if (isEmpty()) return + + while (true) { + val containsNull = getContainsNull() + val key = getKeys() + val n = getN() + + // Capture one complete table generation to allow a read during rehash. + if (key.size != n + 1) continue + + if (containsNull) body(0) + + for (i in 0 until n) { + val k = key[i] + if (k != 0L) body(k) + } + + return + } +} + inline fun Int2ObjectOpenHashMap.getOrCreate(key: Int, body: () -> V): V { get(key)?.let { return it } return body().also { put(key, it) } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MemoryManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MemoryManager.kt index 07145ab4a..097d135d9 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MemoryManager.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/MemoryManager.kt @@ -6,8 +6,8 @@ import mu.KLogging import java.lang.management.ManagementFactory import java.lang.management.MemoryMXBean import java.lang.management.MemoryType -import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicReference import javax.management.Notification import javax.management.NotificationEmitter import javax.management.NotificationListener @@ -19,13 +19,19 @@ class MemoryManager( private val memoryThreshold: Double, private val onOutOfMemory: () -> Unit ) { - private val memoryManagerState = AtomicInteger(STATE_NORMAL) - private val lastGcRequestTime = AtomicLong(0) - private val thresholdBytes = AtomicLong(0) + internal enum class State { + Normal, + SoftReferencesReset, + GcAfterCleanup, + } inner class GCNotificationListener( private val memMx: MemoryMXBean, ) : NotificationListener { + internal val memoryManagerState = AtomicReference(State.Normal) + private val lastGcRequestTime = AtomicLong(0) + private val thresholdBytes = AtomicLong(0) + init { thresholdBytes.set((memMx.heapMemoryUsage.max * memoryThreshold).toLong()) } @@ -49,8 +55,8 @@ class MemoryManager( if (usedAfterGc < thr) { refManager.allSoftRefManagers().asSequence().forEach { it.enable() } - val currentState = memoryManagerState.getAndSet(STATE_NORMAL) - if (currentState != STATE_NORMAL) { + val currentState = memoryManagerState.getAndSet(State.Normal) + if (currentState != State.Normal) { logger.info("Memory back to normal state: $usedAfterGc < $thr") } return @@ -59,7 +65,7 @@ class MemoryManager( logger.info("Detected high memory usage: $usedAfterGc > $thr") when(state) { - STATE_NORMAL -> { + State.Normal -> { var cleaned = -1 refManager.allSoftRefManagers().asSequence().forEach { cleaned += it.cleanup().coerceAtLeast(0) @@ -69,19 +75,19 @@ class MemoryManager( logger.debug("Cleaned soft refs: $cleaned") } - memoryManagerState.compareAndSet(STATE_NORMAL, STATE_SOFT_REF_RESET) + memoryManagerState.compareAndSet(State.Normal, State.SoftReferencesReset) // Ask JVM for another GC; we confirm on the next GC end memMx.gc() } - STATE_SOFT_REF_RESET -> { + State.SoftReferencesReset -> { memMx.gc() - memoryManagerState.compareAndSet(STATE_SOFT_REF_RESET, GC_AFTER_CLEANUP) + memoryManagerState.compareAndSet(State.SoftReferencesReset, State.GcAfterCleanup) lastGcRequestTime.set(info.gcInfo.endTime) } - GC_AFTER_CLEANUP -> { + State.GcAfterCleanup -> { if (info.gcInfo.startTime <= lastGcRequestTime.get() + 10) return if (currentMemoryUsage() < thr) return @@ -138,10 +144,6 @@ class MemoryManager( } companion object { - private const val STATE_NORMAL = 0 - private const val STATE_SOFT_REF_RESET = 1 - private const val GC_AFTER_CLEANUP = 2 - private val logger = object : KLogging() {}.logger private const val DEBUG_DUMP_HEAP_ON_OOM = false diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSetTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSetTest.kt new file mode 100644 index 000000000..9f763ed93 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSetTest.kt @@ -0,0 +1,59 @@ +package org.opentaint.dataflow.ap.ifds + +import kotlinx.collections.immutable.persistentHashSetOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ExclusionSetTest { + private val first = TaintMarkAccessor("first") + private val second = TaintMarkAccessor("second") + private val third = TaintMarkAccessor("third") + + @Test + fun `single-accessor changes keep an eagerly computed hash`() { + val singleton = ExclusionSet.Concrete(first) + assertNotNull(singleton.cachedHash()) + + val added = singleton.add(second) as ExclusionSet.Concrete + assertNotNull(added.cachedHash()) + + val subtracted = added.subtract(first) as ExclusionSet.Concrete + assertNotNull(subtracted.cachedHash()) + } + + @Test + fun `bulk operations defer full hash computation`() { + val left = ExclusionSet.Concrete(first).add(second) as ExclusionSet.Concrete + val right = ExclusionSet.Concrete(second).add(third) as ExclusionSet.Concrete + + val union = left.union(right) as ExclusionSet.Concrete + assertNull(union.cachedHash()) + assertEquals(union.set.hashCode(), union.hashCode()) + assertNotNull(union.cachedHash()) + + val intersection = left.intersect(right) as ExclusionSet.Concrete + assertNull(intersection.cachedHash()) + assertEquals(intersection.set.hashCode(), intersection.hashCode()) + assertNotNull(intersection.cachedHash()) + } + + @Test + fun `equality does not force a deferred hash`() { + val left = ExclusionSet.Concrete(persistentHashSetOf(first, second)) + val right = ExclusionSet.Concrete(persistentHashSetOf(first, second)) + + assertEquals(left, right) + assertNull(left.cachedHash()) + assertNull(right.cachedHash()) + } + + private fun ExclusionSet.Concrete.cachedHash(): Int? = cachedHashField.get(this) as Int? + + private companion object { + val cachedHashField = ExclusionSet.Concrete::class.java.getDeclaredField("cachedHash").apply { + isAccessible = true + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/MethodTaintMarkReachabilityIndexTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/MethodTaintMarkReachabilityIndexTest.kt new file mode 100644 index 000000000..b9ab02b46 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/MethodTaintMarkReachabilityIndexTest.kt @@ -0,0 +1,119 @@ +package org.opentaint.dataflow.ap.ifds + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MethodTaintMarkReachabilityIndexTest { + @Test + fun `finds direct and transitive callers`() { + val index = MethodTaintMarkReachabilityIndex() + index.addCall("entry", "controller") + index.addCall("controller", "sink") + index.addCall("unrelated", "other") + + assertEquals(setOf("sink", "controller", "entry"), index.methodsThatCanReach("sink")) + } + + @Test + fun `uses summary mark transformation between calls`() { + val index = MethodTaintMarkReachabilityIndex() + index.addCall("entry", "transform") + index.addCall("transform", "sink") + index.recordInputMark("transform", "raw") + index.recordExactSummary("transform", "raw", "encoded") + index.recordInputMark("sink", "encoded") + + val reachable = index.statesThatCanReach( + "sink", + setOf("encoded"), + emptyMap(), + relevantMarks = setOf("raw", "encoded"), + ) + + assertTrue(MethodTaintMarkState("entry", "raw") in reachable) + assertFalse(MethodTaintMarkState("entry", "unrelated") in reachable) + } + + @Test + fun `summary without taint marks does not create mark reachability`() { + val index = MethodTaintMarkReachabilityIndex() + index.addCall("entry", "pass") + index.addCall("pass", "sink") + index.recordSummary("pass", emptySet(), emptySet()) + index.recordInputMark("sink", "tainted") + + val reachable = index.statesThatCanReach( + "sink", + setOf("tainted"), + emptyMap(), + relevantMarks = setOf("tainted"), + ) + + assertFalse(MethodTaintMarkState("entry", "tainted") in reachable) + assertEquals(1, index.stats().methods) + } + + @Test + fun `uses rule transitions inside a method`() { + val index = MethodTaintMarkReachabilityIndex() + index.addCall("entry", "sink") + index.recordInputMark("sink", "validated") + + val reachable = index.statesThatCanReach( + targetMethod = "sink", + targetMarks = setOf("validated"), + ruleTransitions = mapOf( + "entry" to setOf(TaintMarkTransition("raw", "validated")), + ), + relevantMarks = setOf("raw", "validated"), + ) + + assertTrue(MethodTaintMarkState("entry", "raw") in reachable) + } + + @Test + fun `ignores summary marks outside the vulnerability rule graph`() { + val index = MethodTaintMarkReachabilityIndex() + index.addCall("entry", "transform") + index.addCall("transform", "sink") + index.recordExactSummary("transform", "raw", "validated") + index.recordExactSummary("transform", "unrelated", "validated") + index.recordInputMark("sink", "validated") + + val reachable = index.statesThatCanReach( + targetMethod = "sink", + targetMarks = setOf("validated"), + ruleTransitions = emptyMap(), + relevantMarks = setOf("raw", "validated"), + ) + + assertTrue(MethodTaintMarkState("transform", "raw") in reachable) + assertTrue(MethodTaintMarkState("entry", "raw") in reachable) + assertFalse(MethodTaintMarkState("transform", "unrelated") in reachable) + assertFalse(MethodTaintMarkState("entry", "unrelated") in reachable) + } + + @Test + fun `ignores rule transitions outside the vulnerability rule graph`() { + val index = MethodTaintMarkReachabilityIndex() + index.addCall("entry", "sink") + index.recordInputMark("sink", "validated") + + val reachable = index.statesThatCanReach( + targetMethod = "sink", + targetMarks = setOf("validated"), + ruleTransitions = mapOf( + "entry" to setOf( + TaintMarkTransition("raw", "validated"), + TaintMarkTransition("unrelated", "validated"), + ), + ), + relevantMarks = setOf("raw", "validated"), + ) + + assertTrue(MethodTaintMarkState("entry", "raw") in reachable) + assertFalse(MethodTaintMarkState("entry", "unrelated") in reachable) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/UnprocessedEdgeListTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/UnprocessedEdgeListTest.kt new file mode 100644 index 000000000..9c9fd411f --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/UnprocessedEdgeListTest.kt @@ -0,0 +1,91 @@ +package org.opentaint.dataflow.ap.ifds + +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import java.lang.reflect.Proxy +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class UnprocessedEdgeListTest { + private val method = object : CommonMethod { + override val name: String = "method" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = error("unused") + } + + private fun statement(name: String) = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod = this@UnprocessedEdgeListTest.method + } + + override fun toString(): String = name + } + + @Test + fun `zero to zero edges are removed before all other edges`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val entryStatement = statement("entry") + val entryPoint = MethodEntryPoint(EmptyMethodContext, entryStatement) + val queue = EdgeCollection.UnprocessedEdgeList(manager, entryPoint) + val zeroFact = manager.createFinalAp(AccessPathBase.This, ExclusionSet.Universe) + + val ordinaryFirst = Edge.ZeroToFact(entryPoint, statement("ordinary-first"), zeroFact) + val zeroFirst = Edge.ZeroToZero(entryPoint, statement("zero-first")) + val ordinaryLast = Edge.ZeroToFact(entryPoint, statement("ordinary-last"), zeroFact) + val zeroLast = Edge.ZeroToZero(entryPoint, statement("zero-last")) + + queue.add(ordinaryFirst) + queue.add(zeroFirst) + queue.add(ordinaryLast) + queue.add(zeroLast) + + assertEquals(4, queue.size) + assertTrue(queue.containsZeroToZeroEdges) + assertEquals(zeroLast, queue.removeLast()) + assertTrue(queue.containsZeroToZeroEdges) + assertEquals(zeroFirst, queue.removeLast()) + assertFalse(queue.containsZeroToZeroEdges) + assertEquals(ordinaryLast, queue.removeLast()) + assertEquals(ordinaryFirst, queue.removeLast()) + assertTrue(queue.isEmpty) + } + + @Test + fun `analyzers with unprocessed zero to zero edges have highest event priority`() { + val zeroToZeroAnalyzer = analyzer(containsZeroToZeroEdges = true, steps = 100) + val earlyOrdinaryAnalyzer = analyzer(containsZeroToZeroEdges = false, steps = 1) + val lateOrdinaryAnalyzer = analyzer(containsZeroToZeroEdges = false, steps = 10) + val nonAnalyzerEvent = Any() + val comparator = TaintAnalysisUnitRunner.EventComparator + + assertTrue(comparator.compare(zeroToZeroAnalyzer, earlyOrdinaryAnalyzer) < 0) + assertTrue(comparator.compare(zeroToZeroAnalyzer, nonAnalyzerEvent) < 0) + assertTrue(comparator.compare(nonAnalyzerEvent, earlyOrdinaryAnalyzer) < 0) + assertTrue(comparator.compare(earlyOrdinaryAnalyzer, lateOrdinaryAnalyzer) < 0) + } + + private fun analyzer(containsZeroToZeroEdges: Boolean, steps: Long): MethodAnalyzer = + Proxy.newProxyInstance( + MethodAnalyzer::class.java.classLoader, + arrayOf(MethodAnalyzer::class.java), + ) { _, method, _ -> + when (method.name) { + "getContainsUnprocessedZeroToZeroEdges" -> containsZeroToZeroEdges + "getAnalyzerSteps" -> steps + else -> error("Unexpected MethodAnalyzer operation: ${method.name}") + } + } as MethodAnalyzer +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/MethodEdgesInitialToFinalApSetTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/MethodEdgesInitialToFinalApSetTest.kt new file mode 100644 index 000000000..a2f03a431 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/MethodEdgesInitialToFinalApSetTest.kt @@ -0,0 +1,132 @@ +package org.opentaint.dataflow.ap.ifds.access + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.LanguageManager +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.automata.AutomataApManager +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.access.cactus.CactusApManager +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.ap.ifds.serialization.MethodContextSerializer +import org.opentaint.dataflow.util.RefManager +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonCallExpr +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MethodEdgesInitialToFinalApSetTest { + private val method = object : CommonMethod { + override val name: String = "dummy" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + + private val statement = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod = this@MethodEdgesInitialToFinalApSetTest.method + } + } + + private val languageManager = object : LanguageManager { + override fun getInstIndex(inst: CommonInst): Int = 0 + override fun getMaxInstIndex(method: CommonMethod): Int = 0 + override fun getInstByIndex(method: CommonMethod, index: Int): CommonInst = statement + override fun isEmpty(method: CommonMethod): Boolean = false + override fun getCallExpr(inst: CommonInst): CommonCallExpr? = null + override fun producesExceptionalControlFlow(inst: CommonInst): Boolean = false + override fun getCalleeMethod(callExpr: CommonCallExpr): CommonMethod = error("unused") + override val methodContextSerializer: MethodContextSerializer get() = error("unused") + } + + @Test + fun `exclusion changes publish the complete final language for every AP implementation`() { + val strategy = AnyAccessorUnrollStrategy.AnyAccessorDisabled + val managers = listOf( + "Tree" to TreeApManager(strategy, RefManager(), org.opentaint.dataflow.util.Cancellation()), + "Automata" to AutomataApManager(strategy, org.opentaint.dataflow.util.Cancellation()), + "Cactus" to CactusApManager(strategy, org.opentaint.dataflow.util.Cancellation()), + "BaseOnly" to BaseOnlyApManager(strategy, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = true), + ) + + managers.forEach { (name, manager) -> + val exclusion1 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-1")) + val exclusion2 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-2")) + val mergedExclusion = exclusion1.union(exclusion2) + val initial1 = manager.mostAbstractInitialAp(AccessPathBase.Return).replaceExclusions(exclusion1) + val initial2 = initial1.replaceExclusions(exclusion2) + val final1 = manager.createFinalAp(AccessPathBase.This, exclusion1) + .prependAccessor(TaintMarkAccessor("mark-1")) + val final2 = manager.createFinalAp(AccessPathBase.This, exclusion2) + .prependAccessor(TaintMarkAccessor("mark-2")) + val edges = manager.methodEdgesInitialToFinalApSet(statement, 0, languageManager) + + assertEquals(1, edges.add(statement, initial1, final1).size, "$name first delta") + val delta = edges.add(statement, initial2, final2) + val stored = mutableListOf>() + edges.collectApAtStatement(stored, statement) + + assertEquals(stored.toSet(), delta.toSet(), "$name must re-emit its complete stored language") + assertTrue(delta.all { it.first.exclusions == mergedExclusion }, "$name initial exclusions") + assertTrue(delta.all { it.second.exclusions == mergedExclusion }, "$name final exclusions") + assertTrue(edges.add(statement, initial2, final2).isEmpty(), "$name duplicate delta") + } + } + + @Test + fun `batch insertion has the same exact delta as scalar insertion`() { + val strategy = AnyAccessorUnrollStrategy.AnyAccessorDisabled + val managers = listOf( + "Tree" to TreeApManager(strategy, RefManager(), org.opentaint.dataflow.util.Cancellation()), + "Automata" to AutomataApManager(strategy, org.opentaint.dataflow.util.Cancellation()), + "Cactus" to CactusApManager(strategy, org.opentaint.dataflow.util.Cancellation()), + "BaseOnly" to BaseOnlyApManager(strategy, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = true), + ) + + managers.forEach { (name, manager) -> + val exclusion = ExclusionSet.Concrete(TaintMarkAccessor("excluded")) + val initials = listOf( + manager.mostAbstractInitialAp(AccessPathBase.This) + .prependAccessor(TaintMarkAccessor("origin-1")) + .replaceExclusions(exclusion), + manager.mostAbstractInitialAp(AccessPathBase.LocalVar(0)) + .prependAccessor(TaintMarkAccessor("origin-2")) + .replaceExclusions(exclusion), + ) + val final = manager.createFinalAp(AccessPathBase.Return, exclusion) + .prependAccessor(TaintMarkAccessor("result")) + val scalar = manager.methodEdgesInitialToFinalApSet(statement, 0, languageManager) + val batch = manager.methodEdgesInitialToFinalApSet(statement, 0, languageManager) + + val scalarDelta = initials.flatMap { scalar.add(statement, it, final) } + val batchDelta = arrayListOf>() + batch.addAll(statement, initials, final) { initial, addedFinal -> + batchDelta += initial to addedFinal + } + + assertEquals(scalarDelta, batchDelta, "$name propagation delta") + + val scalarState = arrayListOf>() + val batchState = arrayListOf>() + scalar.collectApAtStatement(scalarState, statement) + batch.collectApAtStatement(batchState, statement) + assertEquals(scalarState.toSet(), batchState.toSet(), "$name stored relation") + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessPackingTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessPackingTest.kt new file mode 100644 index 000000000..4b996e5e0 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessPackingTest.kt @@ -0,0 +1,65 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlyAccessPackingTest { + private val sentinels = listOf(NO_ACCESSOR, ABSTRACT_MARK, COLLAPSED_MARK) + private val staticReals = listOf(0, 1, 5, 100, BASE_ONLY_STATIC_MASK - BASE_ONLY_BIAS) + private val wideReals = listOf(0, 1, 3, 7, 35, 1000, BASE_ONLY_FIELD_MASK - BASE_ONLY_BIAS) + private val suffixReals = listOf(0, 1, 3, 7, 35, 1000, BASE_ONLY_SUFFIX_VALUE_MASK - BASE_ONLY_BIAS) + + @Test + fun `pack then unpack round-trips every slot including sentinels and max real indices`() { + for (s in sentinels + staticReals) { + for (f in sentinels + wideReals) { + for (x in sentinels + suffixReals) { + val packed = packBaseOnlyAccess(s, f, x) + assertEquals(s, packed.staticIdx, "static slot") + assertEquals(f, packed.fieldIdx, "field slot") + assertEquals(x, packed.suffixIdx, "suffix slot") + packed.withBaseOnlyAccessUnpacked { us, uf, ux -> + assertEquals(s, us, "static via withBaseOnlyAccessUnpacked") + assertEquals(f, uf, "field via withBaseOnlyAccessUnpacked") + assertEquals(x, ux, "suffix via withBaseOnlyAccessUnpacked") + } + } + } + } + } + + @Test + fun `named constants decode to their triples`() { + assertEquals(NO_ACCESSOR, EMPTY_ACCESS.staticIdx) + assertEquals(NO_ACCESSOR, EMPTY_ACCESS.fieldIdx) + assertEquals(NO_ACCESSOR, EMPTY_ACCESS.suffixIdx) + assertTrue(EMPTY_ACCESS.isEmpty) + + assertEquals(NO_ACCESSOR, ABSTRACT_EMPTY_ACCESS.staticIdx) + assertEquals(NO_ACCESSOR, ABSTRACT_EMPTY_ACCESS.fieldIdx) + assertEquals(ABSTRACT_MARK, ABSTRACT_EMPTY_ACCESS.suffixIdx) + assertFalse(ABSTRACT_EMPTY_ACCESS.isEmpty) + assertTrue(ABSTRACT_EMPTY_ACCESS.hasAp) + + assertEquals(NO_ACCESSOR, FINAL_ACCESS.staticIdx) + assertEquals(NO_ACCESSOR, FINAL_ACCESS.fieldIdx) + assertFalse(FINAL_ACCESS.isEmpty) + assertFalse(FINAL_ACCESS.hasAp) + } + + @Test + fun `pack fails fast when a slot overflows its width`() { + assertFailsWith { + packBaseOnlyAccess(BASE_ONLY_STATIC_MASK - BASE_ONLY_BIAS + 1, NO_ACCESSOR, NO_ACCESSOR) + } + assertFailsWith { + packBaseOnlyAccess(NO_ACCESSOR, BASE_ONLY_FIELD_MASK - BASE_ONLY_BIAS + 1, NO_ACCESSOR) + } + assertFailsWith { + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, BASE_ONLY_SUFFIX_VALUE_MASK - BASE_ONLY_BIAS + 1) + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessTest.kt new file mode 100644 index 000000000..71428c1e5 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessTest.kt @@ -0,0 +1,260 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class BaseOnlyAccessTest { + private val accessors = AccessorInterner() + private val ai = BaseOnlyAccessOps + + private val field = FieldAccessor("A", "f", "B") + private val field2 = FieldAccessor("A", "g", "B") + private val mark = TaintMarkAccessor("m") + private val mark2 = TaintMarkAccessor("n") + private val stat = ClassStaticAccessor("T") + private val stat2 = ClassStaticAccessor("U") + private val final = FinalAccessor + + private fun i(a: org.opentaint.dataflow.ap.ifds.Accessor) = accessors.index(a) + + private fun chain(vararg a: org.opentaint.dataflow.ap.ifds.Accessor, abstract: Boolean = false): BaseOnlyAccess = + ai.build(IntArray(a.size) { i(a[it]) }, abstract) + + @Test + fun `equal chains produce equal packed values`() { + assertEquals(chain(mark), chain(mark)) + assertEquals(chain(AnyAccessor, mark), chain(AnyAccessor, mark)) + } + + @Test + fun `field absorbed by any when field-insensitive`() { + val base = chain(AnyAccessor, mark) + assertEquals(base, ai.prepend(base, i(field), fieldSensitive = false)) + } + + @Test + fun `field kept before any when field-sensitive`() { + val base = chain(AnyAccessor, mark) + assertEquals(chain(field, AnyAccessor, mark), ai.prepend(base, i(field), fieldSensitive = true)) + } + + @Test + fun `second field replaces first`() { + val f1 = chain(field, AnyAccessor, mark) + assertEquals(chain(field2, AnyAccessor, mark), ai.prepend(f1, i(field2), fieldSensitive = true)) + } + + @Test + fun `build and append retain the outermost structural accessor`() { + assertEquals(i(field), chain(field, field2, mark).fieldIdx) + assertEquals( + chain(field, mark), + ai.append(chain(field, abstract = true), chain(field2, mark)), + ) + } + + @Test + fun `class static goes before field`() { + val base = chain(field, AnyAccessor, mark) + assertEquals(chain(stat, field, AnyAccessor, mark), ai.prepend(base, i(stat), fieldSensitive = true)) + } + + @Test + fun `prepend taint keeps canonical order behind static`() { + val base = chain(stat) + assertEquals(chain(stat, mark), ai.prepend(base, i(mark), fieldSensitive = false)) + } + + @Test + fun `read field off abstract stays abstract`() { + val abstract = ai.abstractEmpty + assertEquals(abstract, ai.read(abstract, i(field))) + } + + @Test + fun `read any off abstract stays abstract`() { + val abstract = ai.abstractEmpty + assertEquals(abstract, ai.read(abstract, i(AnyAccessor))) + } + + @Test + fun `read field off final is null`() { + assertNull(ai.read(chain(final), i(field))) + } + + @Test + fun `read field off bare taint follows implicit Any`() { + assertEquals(chain(mark), ai.read(chain(mark), i(field))) + } + + @Test + fun `read matching taint drops it to final`() { + assertEquals(chain(final), ai.read(chain(mark), i(mark))) + } + + @Test + fun `read matching field off field-abstract stays abstract`() { + val fieldAbstract = ai.prepend(ai.abstractEmpty, i(field), fieldSensitive = true) + assertEquals(ai.abstractEmpty, ai.read(fieldAbstract, i(field))) + } + + @Test + fun `startsWith structural is true for abstract and bare semantic facts`() { + assertTrue(ai.startsWith(ai.abstractEmpty, i(field))) + assertFalse(ai.startsWith(chain(final), i(field))) + assertTrue(ai.startsWith(chain(mark), i(field))) + } + + @Test + fun `append keeps suffix abstraction when prefix has no terminal`() { + assertEquals(ai.abstractEmpty, ai.append(ai.empty, ai.abstractEmpty)) + } + + @Test + fun `append keeps prefix taint over abstract suffix`() { + assertEquals(chain(mark), ai.append(chain(mark), ai.abstractEmpty)) + } + + @Test + fun `abstract initial yields whole final as delta`() { + val match = ai.matchPrefix(chain(mark), ai.abstractEmpty) + assertFalse(match.emptyDelta) + assertTrue(match.hasSuffix) + assertEquals(chain(mark), match.suffix) + } + + @Test + fun `taint initial does not match bare final`() { + val match = ai.matchPrefix(chain(final), chain(mark)) + assertFalse(match.emptyDelta) + assertFalse(match.hasSuffix) + } + + @Test + fun `read AP-position mirror`() { + val f1 = i(field); val t1 = i(mark); val dollar = i(final) + + // value strict: read field off value -> null (getter-alias removed) + assertNull(ai.read(chain(final), f1)) + // bare mark fact has an implicit structural branch; read own mark -> value + assertEquals(chain(mark), ai.read(chain(mark), f1)) + assertEquals(chain(final), ai.read(chain(mark), t1)) + // suffix-AP: read field idempotent; read mark -> null (must refine, not fabricate) + assertEquals(ai.abstractEmpty, ai.read(ai.abstractEmpty, f1)) + assertNull(ai.read(ai.abstractEmpty, t1)) + // field-AP: read field -> null (refine); static-AP: read anything -> null + assertNull(ai.read(ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1), f1)) + assertNull(ai.read(ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0), t1)) + // committed static advances + assertEquals(ai.abstractEmpty, ai.read(ai.abstractAt(i(stat), NO_ACCESSOR, 2), i(stat))) + } + + @Test + fun `startsWith AP-position truth table`() { + val s1 = i(stat); val s2 = i(stat2); val f1 = i(field) + val t1 = i(mark); val t2 = i(mark2); val dollar = i(final) + + // (ABSTRACT,-1,-1) — AP at static: nothing matches (all refine) + val apStatic = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) + for (q in listOf(s1, s2, f1, t1, dollar)) assertFalse(ai.startsWith(apStatic, q), "apStatic sw $q") + + // (s1, ABSTRACT, -1) — committed s1, AP at field: only s1 + val s1FieldAp = ai.abstractAt(s1, NO_ACCESSOR, 1) + assertTrue(ai.startsWith(s1FieldAp, s1)); assertFalse(ai.startsWith(s1FieldAp, s2)) + assertFalse(ai.startsWith(s1FieldAp, f1)); assertFalse(ai.startsWith(s1FieldAp, t1)) + + // (s1, f1, ABSTRACT) — committed s1.f1, AP at suffix: only s1 at the head + val s1f1SuffAp = ai.abstractAt(s1, f1, 2) + assertTrue(ai.startsWith(s1f1SuffAp, s1)); assertFalse(ai.startsWith(s1f1SuffAp, f1)) + assertFalse(ai.startsWith(s1f1SuffAp, t1)) + + // (-1, ABSTRACT, -1) — AP at field, no static: static false, field false, mark false + val fieldAp = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) + assertFalse(ai.startsWith(fieldAp, s1)); assertFalse(ai.startsWith(fieldAp, f1)) + assertFalse(ai.startsWith(fieldAp, t1)) + + // (-1, -1, ABSTRACT) — AP at suffix: field true ([any]), mark false, static false + val suffAp = ai.abstractEmpty + assertTrue(ai.startsWith(suffAp, f1)); assertFalse(ai.startsWith(suffAp, t1)) + assertFalse(ai.startsWith(suffAp, s1)) + + // concrete bare mark x.!t1.$ : own mark and implicit structural reads are available + val markFact = chain(mark) + assertTrue(ai.startsWith(markFact, f1)); assertTrue(ai.startsWith(markFact, t1)) + assertFalse(ai.startsWith(markFact, t2)); assertFalse(ai.startsWith(markFact, dollar)) + + // value x.$ : strict — only $ + val valueFact = chain(final) + assertTrue(ai.startsWith(valueFact, dollar)); assertFalse(ai.startsWith(valueFact, f1)) + assertFalse(ai.startsWith(valueFact, t1)) + } + + @Test + fun `collapse clears exactly the abstract slot and keeps the rest`() { + val staticAbstract = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) + assertEquals(0, staticAbstract.apSlot) + val staticCollapsed = ai.collapse(staticAbstract) + assertEquals(NO_ACCESSOR, staticCollapsed.staticIdx) + assertFalse(staticCollapsed.isCollapsed) + assertEquals(ai.empty, staticCollapsed) + + val fieldAbstract = ai.abstractAt(i(stat), NO_ACCESSOR, 1) + assertEquals(1, fieldAbstract.apSlot) + val fieldCollapsed = ai.collapse(fieldAbstract) + assertEquals(i(stat), fieldCollapsed.staticIdx) + assertEquals(NO_ACCESSOR, fieldCollapsed.fieldIdx) + assertFalse(fieldCollapsed.isCollapsed) + + val suffixAbstract = ai.abstractAt(i(stat), i(field), 2) + assertEquals(2, suffixAbstract.apSlot) + val suffixCollapsed = ai.collapse(suffixAbstract) + assertTrue(suffixCollapsed.isCollapsed) + assertEquals(i(stat), suffixCollapsed.staticIdx) + assertEquals(i(field), suffixCollapsed.fieldIdx) + + val concrete = chain(mark) + assertEquals(-1, concrete.apSlot) + assertEquals(concrete, ai.collapse(concrete)) + } + + @Test + fun `construction rejects malformed accessor grammar instead of reordering it`() { + val type = TypeInfoAccessor("T") + assertFailsWith { chain(field, stat, mark) } + assertFailsWith { chain(stat, stat2, mark) } + assertFailsWith { chain(mark, field) } + assertFailsWith { chain(ValueAccessor) } + assertFailsWith { + ai.requireCanonical(packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, i(ValueAccessor))) + } + assertFailsWith { chain(TypeInfoGroupAccessor) } + assertFailsWith { chain(TypeInfoGroupAccessor, mark) } + + assertFalse(chain(mark) == chain(ValueAccessor, mark)) + assertEquals(BaseOnlyValueAccessorState.Normal, chain(mark).valueAccessorState) + assertEquals(BaseOnlyValueAccessorState.Value, chain(ValueAccessor, mark).valueAccessorState) + assertFalse(chain(type) == chain(TypeInfoGroupAccessor, type)) + assertEquals(BaseOnlyValueAccessorState.Normal, chain(type).valueAccessorState) + assertEquals(BaseOnlyValueAccessorState.Value, chain(TypeInfoGroupAccessor, type).valueAccessorState) + } + + @Test + fun `prepend rejects an invalid second static or standalone transparent semantic prefix`() { + assertFailsWith { ai.prepend(chain(stat, mark), i(stat2), true) } + assertFailsWith { ai.prepend(chain(final), i(ValueAccessor), true) } + assertFailsWith { ai.prepend(chain(final), i(TypeInfoGroupAccessor), true) } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAnyMatchTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAnyMatchTest.kt new file mode 100644 index 000000000..47251c381 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAnyMatchTest.kt @@ -0,0 +1,90 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.util.Cancellation +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlyAnyMatchTest { + private val arg0 = AccessPathBase.Argument(0) + private val mark = TaintMarkAccessor("m") + private val field = FieldAccessor("A", "f", "B") + + private fun mgr(fieldSensitive: Boolean = false) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.expandedTainted(): FinalFactAp = + createFinalAp(arg0, ExclusionSet.Empty).prependAccessor(mark).prependAccessor(AnyAccessor) + + private fun BaseOnlyApManager.finalSinkReq(): InitialFactAp = + createFinalInitialAp(arg0, ExclusionSet.Empty).prependAccessor(mark) + + private fun BaseOnlyApManager.abstractSinkReq(): InitialFactAp = + mostAbstractInitialAp(arg0).prependAccessor(mark) + + @Test + fun `any-expanded fact satisfies value-itself sink requirement with final accessor`() { + val m = mgr() + val f = m.expandedTainted() + val req = m.finalSinkReq() + assertTrue( + f.contains(req), + "expanded ${(f as BaseOnlyFinalFactAp).access} must contain sink ${(req as BaseOnlyInitialFactAp).access}", + ) + } + + @Test + fun `any-expanded fact satisfies abstract value-itself sink requirement`() { + val m = mgr() + val f = m.expandedTainted() + assertTrue(f.contains(m.abstractSinkReq())) + } + + @Test + fun `field-qualified requirement is covered because fields are absorbed`() { + val m = mgr() + val f = m.expandedTainted() + val req = m.createFinalInitialAp(arg0, ExclusionSet.Empty).prependAccessor(mark).prependAccessor(field) + assertTrue( + f.contains(req), + "value fact ${(f as BaseOnlyFinalFactAp).access} must cover field req ${(req as BaseOnlyInitialFactAp).access}", + ) + } + + @Test + fun `expanded fact does not spuriously match a different mark`() { + val m = mgr() + val f = m.expandedTainted() + val other = m.createFinalInitialAp(arg0, ExclusionSet.Empty).prependAccessor(TaintMarkAccessor("other")) + assertFalse(f.contains(other)) + } + + @Test + fun `expanded fact starts with its terminal mark`() { + val m = mgr() + assertTrue((m.expandedTainted() as BaseOnlyFinalFactAp).let { it.startsWithAccessor(mark) || it.startsWithAccessor(AnyAccessor) }) + assertTrue((m.expandedTainted() as BaseOnlyFinalFactAp).startsWithAccessor(mark)) + } + + @Test + fun `startsWith implies readAccessor is non-null`() { + val m = mgr() + val f = m.expandedTainted() + for (accessor in listOf(mark, field, AnyAccessor)) { + if (f.startsWithAccessor(accessor)) { + assertTrue(f.readAccessor(accessor) != null, "startsWith($accessor) but readAccessor null") + } + } + for (accessor in (f as BaseOnlyFinalFactAp).getStartAccessors()) { + assertTrue(f.readAccessor(accessor) != null, "readAccessor(startAccessor $accessor) null") + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApDeltaConcatTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApDeltaConcatTest.kt new file mode 100644 index 000000000..23a121144 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApDeltaConcatTest.kt @@ -0,0 +1,212 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.tree.AccessPath +import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner +import org.opentaint.dataflow.util.RefManager +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class BaseOnlyApDeltaConcatTest { + private val accessors = AccessorInterner() + private val ai = BaseOnlyAccessOps + + private val field = FieldAccessor("A", "f", "B") + private val field2 = FieldAccessor("A", "g", "B") + private val mark = TaintMarkAccessor("m") + private val stat = ClassStaticAccessor("T") + private val final = FinalAccessor + + private fun i(a: org.opentaint.dataflow.ap.ifds.Accessor) = accessors.index(a) + + private fun chain(vararg a: org.opentaint.dataflow.ap.ifds.Accessor, abstract: Boolean = false): BaseOnlyAccess = + ai.build(IntArray(a.size) { i(a[it]) }, abstract) + + @Test + fun `concat closed fact rejects non-empty delta`() { + val markFact = chain(mark) + assertNull(ai.appendFinal(markFact, chain(mark))) + assertEquals(markFact, ai.appendFinal(markFact, ai.empty)) + } + + @Test + fun `concat suffix-AP widens a field-leading cross-kind delta`() { + val f0Abstract = ai.abstractAt(NO_ACCESSOR, i(field), 2) + val deltaFieldMark = chain(field2, mark) + assertEquals(chain(field, mark), ai.appendFinal(f0Abstract, deltaFieldMark)) + } + + @Test + fun `delta requires initial le final`() { + val c = chain(mark) + val iValue = chain(final) + val m = ai.matchPrefix(c, iValue) + assertFalse(m.emptyDelta) + assertFalse(m.hasSuffix) + } + + @Test + fun `delta abstract initial yields whole final`() { + val c = chain(mark) + val m = ai.matchPrefix(c, ai.abstractEmpty) + assertFalse(m.emptyDelta) + assertTrue(m.hasSuffix) + assertEquals(chain(mark), m.suffix) + } + + @Test + fun `splitConcreteInitial splits a closed value against a fully abstract final`() { + val closedInitial = chain(mark) + val abstractFinal = ai.abstractEmpty + assertFalse(ai.matchPrefix(abstractFinal, closedInitial).emptyDelta) + assertFalse(ai.matchPrefix(abstractFinal, closedInitial).hasSuffix) + val split = ai.splitConcreteInitial(abstractFinal, closedInitial)!! + assertEquals(abstractFinal, split.matched) + assertEquals(chain(mark), split.delta) + } + + @Test + fun `splitConcreteInitial keeps the tail of a closed field initial past a field-abstract final`() { + val closedInitial = chain(field, mark) + val fieldAbstract = ai.abstractAt(NO_ACCESSOR, i(field), 2) + val split = ai.splitConcreteInitial(fieldAbstract, closedInitial)!! + assertEquals(fieldAbstract, split.matched) + assertEquals(chain(mark), split.delta) + } + + @Test + fun `BaseOnly resolves the Stirling semantic sink branch after lossy normalization`() { + val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + val body = FieldAccessor("Response", "Body", "Token") + val sink = TaintMarkAccessor("sink_35") + val bodyIdx = manager.interner.index(body) + val sinkIdx = manager.interner.index(sink) + val summaryAccess = ai.build(intArrayOf(bodyIdx), isAbstract = true) + val callerAccess = ai.build(intArrayOf(sinkIdx), isAbstract = false) + val base = AccessPathBase.Argument(0) + // BaseOnly normalized the Tree union `Body.* | sink_35.$` to + // `Body.* / {sink_35}`, dropping the explicit semantic-mark branch. + val summaryFinal = BaseOnlyFinalFactAp( + manager, + base, + summaryAccess, + ExclusionSet.Concrete(sink), + ) + val callerFact = BaseOnlyInitialFactAp( + manager, + base, + callerAccess, + ExclusionSet.Empty, + ) + val splits = callerFact.splitDelta(summaryFinal) + + assertEquals(1, splits.size, "the structural summary exclusion must not reject a semantic trace mark") + assertEquals(summaryAccess, (splits.single().first as BaseOnlyInitialFactAp).access) + assertEquals( + callerAccess, + (splits.single().second as BaseOnlyNodeInitialDelta).access, + "the sink-only caller suffix must survive as the trace delta", + ) + } + + @Test + fun `Tree resolves the Stirling semantic sink branch retained beside the open body branch`() { + val manager = TreeApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + RefManager(), + Cancellation(), + ) + val body = FieldAccessor("Response", "Body", "Token") + val sink = TaintMarkAccessor("sink_35") + val bodyIdx = manager.interner.index(body) + val sinkIdx = manager.interner.index(sink) + val base = AccessPathBase.Argument(0) + + // This is the Tree summary observed for the same call boundary: the open + // response-body branch and the explicit semantic sink branch coexist. + val bodyBranch = manager.abstractNode.addParent(bodyIdx) + val sinkBranch = manager.finalNode.addParent(sinkIdx) + val summaryFinal = AccessTree( + manager, + base, + bodyBranch.mergeAdd(sinkBranch), + ExclusionSet.Empty, + ) + val callerAccess = AccessPath.AccessNode( + manager, + sinkIdx, + AccessPath.AccessNode(manager, manager.interner.index(FinalAccessor), null), + ) + val callerFact = AccessPath(manager, base, callerAccess, ExclusionSet.Empty) + + val (matched, delta) = callerFact.splitDelta(summaryFinal).single() + assertEquals(callerFact, matched) + assertTrue(delta.isEmpty) + } + + @Test + fun `splitConcreteInitial rejects abstract initial, concrete final, and prefix mismatch`() { + assertNull(ai.splitConcreteInitial(ai.abstractEmpty, ai.abstractEmpty)) + assertNull(ai.splitConcreteInitial(chain(mark), chain(mark))) + assertNull(ai.splitConcreteInitial(ai.abstractAt(NO_ACCESSOR, i(field), 2), chain(field2, mark))) + } + + @Test + fun `AP@base wildcard covers every fact`() { + val apStatic = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) + assertTrue(ai.containsAccess(apStatic, chain(stat, mark))) + assertTrue(ai.containsAccess(apStatic, chain(mark))) + assertTrue(ai.containsAccess(apStatic, chain(field, mark))) + } + + @Test + fun `AP@suffix empty covers static-less terminals including field-carrying`() { + val apSuffixEmpty = ai.abstractEmpty + assertTrue(ai.containsAccess(apSuffixEmpty, chain(mark))) + assertFalse(ai.containsAccess(apSuffixEmpty, chain(stat, mark))) + assertTrue(ai.containsAccess(apSuffixEmpty, chain(field, mark))) + } + + @Test + fun `AP@suffix containment is field-lenient after lossy projection`() { + val apSuffixField = ai.abstractAt(NO_ACCESSOR, i(field), 2) + assertTrue(ai.containsAccess(apSuffixField, chain(field, mark))) + assertTrue(ai.containsAccess(apSuffixField, chain(mark))) + assertFalse(ai.covers(apSuffixField, chain(mark)), "storage subsumption remains directional") + } + + @Test + fun `splitConcreteInitial known-empty field is field-lenient`() { + val apSuffixEmpty = ai.abstractEmpty + val fieldSplit = ai.splitConcreteInitial(apSuffixEmpty, chain(field, mark))!! + assertEquals(apSuffixEmpty, fieldSplit.matched) + assertEquals(chain(mark), fieldSplit.delta) + val split = ai.splitConcreteInitial(apSuffixEmpty, chain(mark))!! + assertEquals(chain(mark), split.delta) + } + + @Test + fun `trace append accepts a cross-kind terminal delta at an AP@static prefix`() { + val apStaticPrefix = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) + val result = ai.append(apStaticPrefix, chain(mark)) + assertNotNull(result) + assertEquals(chain(mark), result) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAppendFinalTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAppendFinalTest.kt new file mode 100644 index 000000000..2e9d432c8 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAppendFinalTest.kt @@ -0,0 +1,57 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class BaseOnlyAppendFinalTest { + private val accessors = AccessorInterner() + private val ai = BaseOnlyAccessOps + private val field = FieldAccessor("A", "f", "B") + private val field2 = FieldAccessor("A", "g", "B") + private val mark = TaintMarkAccessor("m") + private val stat = ClassStaticAccessor("T") + private fun i(a: org.opentaint.dataflow.ap.ifds.Accessor) = accessors.index(a) + private fun chain(vararg a: org.opentaint.dataflow.ap.ifds.Accessor, abstract: Boolean = false): BaseOnlyAccess = + ai.build(IntArray(a.size) { i(a[it]) }, abstract) + + // same-kind splices succeed (receiver hole slot == delta first-accessor slot) + @Test fun `AP@static receiver accepts a static-leading delta`() { + val recv = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) // (-2,-1,-1) + assertEquals(chain(stat, mark), ai.appendFinal(recv, chain(stat, mark))) + } + @Test fun `AP@suffix receiver accepts a terminal-leading delta`() { + val recv = ai.abstractAt(NO_ACCESSOR, i(field), 2) // (-1,f,-2) + assertEquals(chain(field, mark), ai.appendFinal(recv, chain(mark))) + } + @Test fun `empty delta is identity`() { + val recv = ai.abstractEmpty + assertEquals(recv, ai.appendFinal(recv, ai.empty)) + } + + // A representational category mismatch is widened rather than rejected. + @Test fun `AP@suffix receiver retains terminal after absorbing a field-leading semantic delta`() { + val recv = ai.abstractAt(NO_ACCESSOR, i(field), 2) // (-1,f,-2), hole at slot 2 + assertEquals(chain(field, mark), ai.appendFinal(recv, chain(field2, mark))) + } + @Test fun `root suffix receiver preserves implicit Any when absorbing a field-leading semantic delta`() { + val recv = ai.abstractEmpty // (-1,-1,-2), implicit Any + val expected = chain(AnyAccessor, mark) // (-1,-1,m) + assertEquals(expected, ai.append(recv, chain(field2, mark))) + assertEquals(expected, ai.appendFinal(recv, chain(field2, mark))) + } + @Test fun `AP@suffix receiver abstracts after retained field for a field-leading exact delta`() { + val recv = ai.abstractAt(NO_ACCESSOR, i(field), 2) + assertEquals(recv, ai.appendFinal(recv, chain(field2, FinalAccessor))) + } + @Test fun `AP@field receiver rejects a static-leading delta`() { + val recv = ai.abstractAt(i(stat), NO_ACCESSOR, 1) // (s,-2,-1), hole at slot 1 + assertNull(ai.appendFinal(recv, chain(stat, mark))) // delta leads at slot 0 + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyClearTableTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyClearTableTest.kt new file mode 100644 index 000000000..0332014a0 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyClearTableTest.kt @@ -0,0 +1,218 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals + +// Pin for BaseOnlyAccessOps.clear (the `clearAccessor` operation, spec: +// docs/superpowers/specs/2026-07-13-baseonly-clearaccessor-spec.md). Over the full enumerated +// fact x accessor universe (both modes) it asserts the implementation equals `expectedClear`, +// the spec's denotational reference: clearAccessor(a) = drop every path that begins with `a`. +// Ordinarily this kills a fact exactly when `a` is its first accessor. The compact value-accessor +// state makes Normal and Value roots distinct, so clear removes exactly one fact. +class BaseOnlyClearTableTest { + private val base = AccessPathBase.Argument(0) + + private val s1 = ClassStaticAccessor("S1") + private val s2 = ClassStaticAccessor("S2") + private val f1 = FieldAccessor("C", "f1", "T") + private val f2 = FieldAccessor("C", "f2", "T") + private val t1 = TaintMarkAccessor("t1") + private val t2 = TaintMarkAccessor("t2") + private val ty1 = TypeInfoAccessor("pkg.Ty1") + + private enum class Suffix { ABSTRACT, VALUE, MARK1, MARK2, TYPE } + + private fun mgr(fieldSensitive: Boolean) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.mkAccess(staticIdx: Int, fieldIdx: Int, suffix: Suffix): BaseOnlyAccess { + val idxs = ArrayList(3) + if (staticIdx != NO_ACCESSOR) idxs.add(staticIdx) + if (fieldIdx != NO_ACCESSOR) idxs.add(fieldIdx) + var isAbstract = false + when (suffix) { + Suffix.ABSTRACT -> isAbstract = true + Suffix.VALUE -> idxs.add(FINAL_ACCESSOR_IDX) + Suffix.MARK1 -> idxs.add(interner.index(t1)) + Suffix.MARK2 -> idxs.add(interner.index(t2)) + Suffix.TYPE -> idxs.add(interner.index(ty1)) + } + return BaseOnlyAccessOps.build(idxs.toIntArray(), isAbstract) + } + + private fun BaseOnlyApManager.statics(): List = + listOf(NO_ACCESSOR, interner.index(s1), interner.index(s2)) + + private fun BaseOnlyApManager.fields(): List = + if (fieldSensitive) listOf(NO_ACCESSOR, ANY_ACCESSOR_IDX, interner.index(f1), interner.index(f2), ELEMENT_ACCESSOR_IDX) + else listOf(NO_ACCESSOR, ANY_ACCESSOR_IDX) + + private fun BaseOnlyApManager.facts(): List { + val out = LinkedHashSet() + for (st in statics()) for (fl in fields()) { + for (sf in Suffix.values()) out.add(mkAccess(st, fl, sf)) + } + out.add(BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0)) + for (st in statics()) out.add(BaseOnlyAccessOps.abstractAt(st, NO_ACCESSOR, 1)) + return out.toList() + } + + private fun BaseOnlyApManager.clearIdxs(): List> = listOf( + "s1" to interner.index(s1), + "s2" to interner.index(s2), + "f1" to interner.index(f1), + "f2" to interner.index(f2), + "[el]" to ELEMENT_ACCESSOR_IDX, + "ANY" to ANY_ACCESSOR_IDX, + "\$" to FINAL_ACCESSOR_IDX, + "!t1" to interner.index(t1), + "!t2" to interner.index(t2), + "val" to interner.index(ValueAccessor), + "tig" to TYPE_INFO_GROUP_ACCESSOR_IDX, + "ty1" to interner.index(ty1), + ) + + private fun BaseOnlyApManager.label(idx: Int): String = when (idx) { + interner.index(s1) -> "s1" + interner.index(s2) -> "s2" + interner.index(f1) -> "f1" + interner.index(f2) -> "f2" + interner.index(t1) -> "t1" + interner.index(t2) -> "t2" + interner.index(ValueAccessor) -> "val" + interner.index(ty1) -> "ty1" + ELEMENT_ACCESSOR_IDX -> "[el]" + ANY_ACCESSOR_IDX -> "ANY" + TYPE_INFO_GROUP_ACCESSOR_IDX -> "tig" + else -> "#$idx" + } + + private fun BaseOnlyApManager.render(a: BaseOnlyAccess, root: String): String { + val sb = StringBuilder(root) + when { + a.staticIdx == ABSTRACT_MARK -> sb.append(".*s") + a.staticIdx >= 0 -> sb.append(".").append(label(a.staticIdx)) + } + when { + a.fieldIdx == ABSTRACT_MARK -> sb.append(".*f") + a.fieldIdx >= 0 -> sb.append(".").append(label(a.fieldIdx)) + } + if (a.suffixIdx >= 0) { + when { + a.suffixIdx.isTypeInfoAccessor() -> sb.append(".tig.").append(label(a.suffixIdx)) + a.hasSemanticMark -> sb.append(".!").append(label(a.suffixIdx)) + } + sb.append(".$") + } + if (a.isSuffixAbstract) sb.append(".*") + return sb.toString() + } + + // Reference clearAccessor, independent of the implementation: clearAccessor(a) removes every + // ground path that begins with `a`. On a single BaseOnly path that is: + // - head absent: the direct path is unaffected unless its own suffix is removed; + // - a == first accessor -> null; + // - otherwise -> keep. + // Compact terminals retain their covering state when either one of their two root branches is + // removed. Clear never strips and promotes a tail; that is readAccessor. + private fun firstAccessor(a: BaseOnlyAccess): Int? = when { + a.staticIdx >= 0 -> a.staticIdx + a.fieldIdx >= 0 -> a.fieldIdx + a.suffixIdx < 0 -> null + a.suffixIdx == FINAL_ACCESSOR_IDX -> FINAL_ACCESSOR_IDX + a.suffixIdx.isTypeInfoAccessor() && a.valueAccessorState == BaseOnlyValueAccessorState.Value -> + TYPE_INFO_GROUP_ACCESSOR_IDX + else -> a.suffixIdx + } + + private fun expectedClear(access: BaseOnlyAccess, idx: Int): BaseOnlyAccess? { + if (access.staticIdx == NO_ACCESSOR && access.fieldIdx == NO_ACCESSOR && access.hasSemanticMark) { + return access + } + val head = firstAccessor(access) ?: return access + if (head != idx) return access + return null + } + + // cell text: current result, and "|ref" appended only when the reference differs. + // · = result equals the input fact (unchanged / kept) + // ∅ = null (fact dropped) + // x… = the rendered surviving access path + // trailing * on the whole cell = guarded-reachable (startsWith(accessor)==true) + private fun BaseOnlyApManager.cellText(fact: BaseOnlyAccess, idx: Int): String { + val cur = BaseOnlyAccessOps.clear(fact, idx) + val ref = expectedClear(fact, idx) + fun show(r: BaseOnlyAccess?): String = when { + r == null -> "∅" + r == fact -> "·" + else -> render(r, "x") + } + val curS = show(cur) + val body = if (cur == ref) curS else "$curS|${show(ref)}" + return body + if (BaseOnlyAccessOps.startsWith(fact, idx)) "*" else "" + } + + private fun dump(m: BaseOnlyApManager): String { + val sb = StringBuilder() + val facts = m.facts() + val cols = m.clearIdxs() + + sb.appendLine("================================================================") + sb.appendLine("BASE-ONLY clearAccessor — full result table — mode fieldSensitive=${m.fieldSensitive}") + sb.appendLine("cell = clear(fact, accessor). '·'=kept unchanged '∅'=null(dropped) 'x…'=surviving path") + sb.appendLine(" 'cur|ref' when current diverges from the Tree/Automata reference") + sb.appendLine(" trailing '*' = guarded-reachable (startsWith(accessor)==true)") + sb.appendLine("================================================================") + sb.appendLine() + + val w = 14 + sb.append(" %-17s".format("fact \\ clear")) + for ((name, _) in cols) sb.append("%-${w}s".format(name)) + sb.appendLine() + for (a in facts) { + sb.append(" %-17s".format(m.render(a, "x"))) + for ((_, idx) in cols) sb.append("%-${w}s".format(m.cellText(a, idx))) + sb.appendLine() + } + sb.appendLine() + return sb.toString() + } + + private fun run(mode: Int) { + val m = mgr(mode >= 1) + for (a in m.facts()) { + for ((name, idx) in m.clearIdxs()) { + assertEquals( + expectedClear(a, idx), + BaseOnlyAccessOps.clear(a, idx), + "clear(${m.render(a, "x")}, $name) must equal the clearAccessor spec", + ) + } + } + val out = dump(m) + val f = File("/tmp/claude-1002/-drive-testcomp-opentaint-go-rules-opentaint/5f02fec5-1d3b-4bbb-9f1b-6cc2b877e6a5/scratchpad/clear-investigation/clear_mode$mode.txt") + f.parentFile.mkdirs() + f.writeText(out) + } + + @Test + fun `clear matches spec mode0`() = run(0) + + @Test + fun `clear matches spec mode1`() = run(1) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyContainsTableTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyContainsTableTest.kt new file mode 100644 index 000000000..164b344b1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyContainsTableTest.kt @@ -0,0 +1,237 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals + +class BaseOnlyContainsTableTest { + private val base = AccessPathBase.Argument(0) + private val other = AccessPathBase.Argument(1) + + private val s1 = ClassStaticAccessor("S1") + private val s2 = ClassStaticAccessor("S2") + private val f1 = FieldAccessor("C", "f1", "T") + private val f2 = FieldAccessor("C", "f2", "T") + private val t1 = TaintMarkAccessor("t1") + private val t2 = TaintMarkAccessor("t2") + + private enum class Suffix { ABSTRACT, VALUE, MARK1, MARK2 } + + private fun mgr(fieldSensitive: Boolean) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.mkAccess(staticIdx: Int, fieldIdx: Int, suffix: Suffix): BaseOnlyAccess { + val idxs = ArrayList(3) + if (staticIdx != NO_ACCESSOR) idxs.add(staticIdx) + if (fieldIdx != NO_ACCESSOR) idxs.add(fieldIdx) + var isAbstract = false + when (suffix) { + Suffix.ABSTRACT -> isAbstract = true + Suffix.VALUE -> idxs.add(FINAL_ACCESSOR_IDX) + Suffix.MARK1 -> idxs.add(interner.index(t1)) + Suffix.MARK2 -> idxs.add(interner.index(t2)) + } + return BaseOnlyAccessOps.build(idxs.toIntArray(), isAbstract) + } + + private fun BaseOnlyApManager.statics(): List = + listOf(NO_ACCESSOR, interner.index(s1), interner.index(s2)) + + private fun BaseOnlyApManager.fields(): List = + if (fieldSensitive) listOf(NO_ACCESSOR, interner.index(f1), interner.index(f2), ELEMENT_ACCESSOR_IDX) + else listOf(NO_ACCESSOR) + + private fun BaseOnlyApManager.facts(): List { + val out = LinkedHashSet() + for (st in statics()) for (fl in fields()) { + for (sf in Suffix.values()) out.add(mkAccess(st, fl, sf)) + } + out.add(BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0)) + for (st in statics()) out.add(BaseOnlyAccessOps.abstractAt(st, NO_ACCESSOR, 1)) + return out.toList() + } + + private fun BaseOnlyApManager.label(idx: Int): String = when (idx) { + interner.index(s1) -> "s1" + interner.index(s2) -> "s2" + interner.index(f1) -> "f1" + interner.index(f2) -> "f2" + interner.index(t1) -> "t1" + interner.index(t2) -> "t2" + ELEMENT_ACCESSOR_IDX -> "[el]" + else -> "#$idx" + } + + private fun BaseOnlyApManager.slot(idx: Int): String = when (idx) { + NO_ACCESSOR -> "-1" + ABSTRACT_MARK -> "*" + FINAL_ACCESSOR_IDX -> "$" + else -> label(idx) + } + + private fun BaseOnlyApManager.render(a: BaseOnlyAccess, root: String): String { + val sb = StringBuilder(root) + when { + a.staticIdx == ABSTRACT_MARK -> sb.append(".*s") + a.staticIdx >= 0 -> sb.append(".").append(label(a.staticIdx)) + } + when { + a.fieldIdx == ABSTRACT_MARK -> sb.append(".*f") + a.fieldIdx >= 0 -> sb.append(".").append(label(a.fieldIdx)) + } + if (a.suffixIdx >= 0) { + if (a.hasSemanticMark) sb.append(".!").append(label(a.suffixIdx)) + sb.append(".$") + } + if (a.isSuffixAbstract) sb.append(".*") + return sb.toString() + } + + private fun BaseOnlyApManager.tag(a: BaseOnlyAccess): String = when { + a.isEmpty -> "empty" + a.hasAp -> "ap@${a.apSlot}" + a.hasSemanticMark -> "mark" + a.suffixIdx == FINAL_ACCESSOR_IDX -> "value" + else -> "open" + } + + private fun dump(m: BaseOnlyApManager): String { + val sb = StringBuilder() + val facts = m.facts() + val labels = facts.map { m.render(it, "x") } + + sb.appendLine("================================================================") + sb.appendLine("BASE-ONLY contains PIN — mode fieldSensitive=${m.fieldSensitive}") + sb.appendLine("cell = F_row(final).contains(F_col(initial)); T = contained, . = not") + sb.appendLine("contains(i) = sameBase && containsProjected(access, i.access) [directional coverage plus the documented missing-structural projection match]") + sb.appendLine("================================================================") + sb.appendLine() + + sb.appendLine("## FACTS (${facts.size})") + facts.forEachIndexed { i, a -> + sb.appendLine(" F%02d = %-14s (%-4s %-4s %-4s) [%s]".format(i, labels[i], m.slot(a.staticIdx), m.slot(a.fieldIdx), m.slot(a.suffixIdx), m.tag(a))) + } + sb.appendLine() + + val contains = Array(facts.size) { fi -> + val finalAp = BaseOnlyFinalFactAp(m, base, facts[fi], ExclusionSet.Empty) + BooleanArray(facts.size) { ii -> + finalAp.contains(BaseOnlyInitialFactAp(m, base, facts[ii], ExclusionSet.Empty)) + } + } + + sb.appendLine("## CONTAINS MATRIX cell = F_row.contains(F_col)") + sb.append(" ") + for (ii in facts.indices) sb.append("%-4s".format("F%02d".format(ii))) + sb.appendLine() + for (fi in facts.indices) { + sb.append(" F%02d ".format(fi)) + for (ii in facts.indices) sb.append("%-4s".format(if (contains[fi][ii]) "T" else ".")) + sb.appendLine() + } + sb.appendLine() + + sb.appendLine("## PER-FACT BREAKDOWN (initials each final contains; self omitted)") + for (fi in facts.indices) { + val hits = facts.indices.filter { it != fi && contains[fi][it] } + if (hits.isEmpty()) continue + sb.appendLine(" %-14s contains: %s".format(labels[fi], hits.joinToString(", ") { labels[it] })) + } + sb.appendLine() + + // off-diagonal true cells classified + sb.appendLine("## OFF-DIAGONAL TRUE CELLS (mechanism)") + var offDiag = 0 + for (fi in facts.indices) for (ii in facts.indices) { + if (fi == ii || !contains[fi][ii]) continue + offDiag++ + val cc = BaseOnlyAccessOps.containsAccess(facts[fi], facts[ii]) + val mech = when { + !cc -> "identity (non-identity!)" + facts[fi].hasAp -> "containsAccess(abstract-prefix wildcard)" + else -> "covers(directional virtual field-[any]; suffix+static exact)" + } + sb.appendLine(" %-14s contains %-14s : %s".format(labels[fi], labels[ii], mech)) + } + if (offDiag == 0) sb.appendLine(" (none — contains is pure identity in this mode)") + sb.appendLine() + + // cross-base probe: does the first clause leak across bases? + sb.appendLine("## CROSS-BASE PROBE x-fact.contains(y-same-access)") + var leak = 0 + for (fi in facts.indices) { + val xFinal = BaseOnlyFinalFactAp(m, base, facts[fi], ExclusionSet.Empty) + val yInit = BaseOnlyInitialFactAp(m, other, facts[fi], ExclusionSet.Empty) + if (xFinal.contains(yInit)) { leak++; if (leak <= 3) sb.appendLine(" LEAK: x.${labels[fi].removePrefix("x")} .contains(y.same) = true") } + } + sb.appendLine(" cross-base identical-access contained count = $leak / ${facts.size}") + sb.appendLine() + return sb.toString() + } + + private fun pin(mode: Int) { + val m = mgr(mode >= 1) + val actual = dump(m) + val scratch = File("/tmp/claude-1002/-drive-testcomp-opentaint-go-rules-opentaint/597d4672-dd12-411f-bbdb-d64b06ae40cd/scratchpad/contains_mode$mode.txt") + scratch.parentFile.mkdirs() + scratch.writeText(actual) + val golden = javaClass.getResource("/baseonly/contains_pin_mode$mode.golden.txt") + if (golden == null) { + println("PIN contains mode$mode: no golden resource yet — wrote actual to ${scratch.path}") + } else { + fun String.normalizeLineEnds(): String = + lineSequence().joinToString("\n") { it.trimEnd() }.trimEnd() + assertEquals( + golden.readText().normalizeLineEnds(), + actual.normalizeLineEnds(), + "contains behaviour changed for mode $mode", + ) + } + } + + @Test + fun `pin mode0`() = pin(0) + + @Test + fun `pin mode1`() = pin(1) + + // Proves the enumerated fact space contains every abstraction-point fact the engine + // can produce: (a) the abstractAt(static, field, slot) universe for all slots, and + // (b) every initial/final pair emitted by the real abstraction machinery over every + // concrete fact with all accessors excluded (which forces emission at every point). + @Test + fun `enumeration covers all abstraction points`() { + val m = mgr(true) + val current = m.facts().toSet() + + val abstractAtUniverse = LinkedHashSet() + for (st in m.statics()) for (fl in m.fields()) for (slot in 0..2) abstractAtUniverse.add(BaseOnlyAccessOps.abstractAt(st, fl, slot)) + val missingAbstractAt = abstractAtUniverse - current + assertEquals(emptySet(), missingAbstractAt, "abstractAt abstraction points not enumerated") + + val abstraction = BaseOnlyInitialFactAbstraction(m) + val excl = listOf(s1, s2, f1, f2, t1, t2) + .fold(ExclusionSet.Empty) { acc, a -> acc.add(a) } + val emitted = LinkedHashSet() + for (st in m.statics()) for (fl in m.fields()) for (sf in Suffix.values()) { + val concrete = m.mkAccess(st, fl, sf) + abstraction.registerNewInitialFact(BaseOnlyInitialFactAp(m, base, concrete, excl), FactTypeChecker.Dummy) + abstraction.addAbstractedInitialFact(BaseOnlyFinalFactAp(m, base, concrete, ExclusionSet.Empty), FactTypeChecker.Dummy) + .forEach { (i, f) -> + emitted.add((i as BaseOnlyInitialFactAp).access) + emitted.add((f as BaseOnlyFinalFactAp).access) + } + } + val emittedNotEnumerated = emitted - current + assertEquals(emptySet(), emittedNotEnumerated, "engine-emitted abstraction facts not enumerated") + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaConcatPinTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaConcatPinTest.kt new file mode 100644 index 000000000..62864396d --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaConcatPinTest.kt @@ -0,0 +1,213 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals + +class BaseOnlyDeltaConcatPinTest { + private val base = AccessPathBase.Argument(0) + + private val s1 = ClassStaticAccessor("S1") + private val s2 = ClassStaticAccessor("S2") + private val f1 = FieldAccessor("C", "f1", "T") + private val f2 = FieldAccessor("C", "f2", "T") + private val t1 = TaintMarkAccessor("t1") + private val t2 = TaintMarkAccessor("t2") + + // empty is not a fact (it means "no fact"); every fact carries a terminal (abstract or mark). + // value(.$) and collapsed(.^) are transient and never persist as domain facts. + private enum class Suffix { ABSTRACT, MARK1, MARK2 } + + private fun mgr(fieldSensitive: Boolean) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.mkAccess(static: ClassStaticAccessor?, field: FieldAccessor?, suffix: Suffix): BaseOnlyAccess { + val idxs = ArrayList(3) + if (static != null) idxs.add(interner.index(static)) + if (field != null) idxs.add(interner.index(field)) + var isAbstract = false + when (suffix) { + Suffix.ABSTRACT -> isAbstract = true + Suffix.MARK1 -> idxs.add(interner.index(t1)) + Suffix.MARK2 -> idxs.add(interner.index(t2)) + } + return BaseOnlyAccessOps.build(idxs.toIntArray(), isAbstract) + } + + // ---- enumerate the representable fact space for a mode ---- + private fun BaseOnlyApManager.facts(): List { + val statics = listOf(null, s1, s2) + val fields = if (fieldSensitive) listOf(null, f1, f2) else listOf(null) + val suffixes = Suffix.values().toList() + val out = ArrayList() + for (st in statics) for (fl in fields) for (sf in suffixes) out.add(mkAccess(st, fl, sf)) + out.add(BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0)) + for (st in statics) { + val staticIdx = if (st != null) interner.index(st) else NO_ACCESSOR + out.add(BaseOnlyAccessOps.abstractAt(staticIdx, NO_ACCESSOR, 1)) + } + return out + } + + // ---- canonical, mode-independent rendering (stable, no interner-index leakage) ---- + private fun BaseOnlyApManager.label(idx: Int): String = when (idx) { + interner.index(s1) -> "s1" + interner.index(s2) -> "s2" + interner.index(f1) -> "f1" + interner.index(f2) -> "f2" + interner.index(t1) -> "t1" + interner.index(t2) -> "t2" + else -> "#$idx" + } + + private fun BaseOnlyApManager.render(a: BaseOnlyAccess, root: String): String { + val sb = StringBuilder(root) + when { + a.staticIdx == ABSTRACT_MARK -> sb.append(".*s") + a.staticIdx >= 0 -> sb.append(".").append(label(a.staticIdx)) + } + when { + a.fieldIdx == ABSTRACT_MARK -> sb.append(".*f") + a.fieldIdx >= 0 -> sb.append(".").append(label(a.fieldIdx)) + } + if (a.suffixIdx >= 0) { + if (a.hasSemanticMark) sb.append(".!").append(label(a.suffixIdx)) + sb.append(".$") + } + if (a.isSuffixAbstract) sb.append(".*") + return sb.toString() + } + + private fun BaseOnlyApManager.renderFact(a: BaseOnlyAccess): String { + val body = render(a, "x") + val tag = when { + a.isEmpty -> "empty" + a.hasAp -> "ap@${a.apSlot}" + a.hasSemanticMark -> "mark" + a.suffixIdx == FINAL_ACCESSOR_IDX -> "value" + else -> "open" + } + return "%-14s (%2d,%2d,%2d) [%s]".format(body, a.staticIdx, a.fieldIdx, a.suffixIdx, tag) + } + + private fun BaseOnlyApManager.renderDelta(d: BaseOnlyFinalDelta): String = when (d) { + BaseOnlyEmptyFinalDelta -> "ε" + is BaseOnlyNodeFinalDelta -> render(d.access, "Δ") + } + + private fun dump(m: BaseOnlyApManager): String { + val sb = StringBuilder() + val facts = m.facts() + + sb.appendLine("================================================================") + sb.appendLine("BASE-ONLY delta/concat PIN — mode fieldSensitive=${m.fieldSensitive}") + sb.appendLine("slots=(static,field,suffix) suffix: -2=abstract(*) 3=value(\$) >=0-other=mark (empty is not a fact)") + sb.appendLine("================================================================") + sb.appendLine() + + sb.appendLine("## FACTS (${facts.size})") + facts.forEachIndexed { i, a -> sb.appendLine(" F%02d = %s".format(i, m.renderFact(a))) } + sb.appendLine() + + // ---- all pairwise deltas: final.delta(initial) ---- + val deltaKeyToId = LinkedHashMap() + val deltaRender = ArrayList() + fun deltaId(d: BaseOnlyFinalDelta): Int { + val key = m.renderDelta(d) + return deltaKeyToId.getOrPut(key) { deltaRender.add(key); deltaRender.size - 1 } + } + // deterministic discovery order: iterate finals then initials + val cell = Array(facts.size) { arrayOfNulls(facts.size) } + for (fi in facts.indices) { + val finalAp = BaseOnlyFinalFactAp(m, base, facts[fi], ExclusionSet.Empty) + for (ii in facts.indices) { + val initAp = BaseOnlyInitialFactAp(m, base, facts[ii], ExclusionSet.Empty) + val deltas = finalAp.delta(initAp) + cell[fi][ii] = if (deltas.isEmpty()) "-" else + deltas.joinToString(",") { "D%d".format(deltaId(it as BaseOnlyFinalDelta)) } + } + } + + sb.appendLine("## DISTINCT DELTAS (${deltaRender.size}) [from all ${facts.size}x${facts.size} ordered pairs final.delta(initial)]") + deltaRender.forEachIndexed { i, r -> sb.appendLine(" D%02d = %s".format(i, r)) } + sb.appendLine(" ('-' in the matrix below = NO-MATCH, empty delta list)") + sb.appendLine() + + sb.appendLine("## DELTA MATRIX cell = F_row.delta(F_col)") + sb.append(" ") + for (ii in facts.indices) sb.append("| %-7s".format("F%02d".format(ii))) + sb.appendLine() + for (fi in facts.indices) { + sb.append(" F%02d ".format(fi)) + for (ii in facts.indices) sb.append("| %-7s".format(cell[fi][ii])) + sb.appendLine() + } + sb.appendLine() + + // ---- all concatenations: fact.concat(delta) ---- + sb.appendLine("## CONCAT MATRIX cell = F_row.concat(D_col)") + sb.append(" ") + for (di in deltaRender.indices) sb.append("| %-14s".format("D%02d".format(di))) + sb.appendLine() + // reconstruct delta objects by id (need the actual object; rebuild from a representative pair scan) + val deltaObjById = arrayOfNulls(deltaRender.size) + for (fi in facts.indices) { + val finalAp = BaseOnlyFinalFactAp(m, base, facts[fi], ExclusionSet.Empty) + for (ii in facts.indices) { + for (d in finalAp.delta(BaseOnlyInitialFactAp(m, base, facts[ii], ExclusionSet.Empty))) { + val id = deltaId(d as BaseOnlyFinalDelta) + if (deltaObjById[id] == null) deltaObjById[id] = d + } + } + } + for (fi in facts.indices) { + val finalAp = BaseOnlyFinalFactAp(m, base, facts[fi], ExclusionSet.Empty) + sb.append(" F%02d ".format(fi)) + for (di in deltaRender.indices) { + val d = deltaObjById[di]!! + val res = finalAp.concat(FactTypeChecker.Dummy, d) as BaseOnlyFinalFactAp? + sb.append("| %-14s".format(res?.let { m.render(it.access, "x") } ?: "null")) + } + sb.appendLine() + } + sb.appendLine() + return sb.toString() + } + + private fun pin(mode: Int) { + val m = mgr(mode >= 1) + val actual = dump(m) + + // mirror to scratchpad for review + val scratch = File("/tmp/claude-1002/-drive-testcomp-opentaint-go-rules-opentaint/597d4672-dd12-411f-bbdb-d64b06ae40cd/scratchpad/pin_mode$mode.txt") + scratch.parentFile.mkdirs() + scratch.writeText(actual) + + val golden = javaClass.getResource("/baseonly/delta_concat_pin_mode$mode.golden.txt") + if (golden == null) { + println("PIN mode$mode: no golden resource yet — wrote actual to ${scratch.path}") + } else { + fun String.normalizeLineEnds(): String = + lineSequence().joinToString("\n") { it.trimEnd() }.trimEnd() + assertEquals( + golden.readText().normalizeLineEnds(), + actual.normalizeLineEnds(), + "delta/concat behaviour changed for mode $mode", + ) + } + } + + @Test + fun `pin mode0`() = pin(0) + + @Test + fun `pin mode1`() = pin(1) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaEnumTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaEnumTest.kt new file mode 100644 index 000000000..9cbbb0369 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaEnumTest.kt @@ -0,0 +1,83 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlyDeltaEnumTest { + private val accessors = AccessorInterner() + private val ai = BaseOnlyAccessOps + private val field = FieldAccessor("A", "f", "B") + private val mark = TaintMarkAccessor("m") + private val stat = ClassStaticAccessor("T") + private fun i(a: org.opentaint.dataflow.ap.ifds.Accessor) = accessors.index(a) + private fun chain(vararg a: org.opentaint.dataflow.ap.ifds.Accessor, abstract: Boolean = false): BaseOnlyAccess = + ai.build(IntArray(a.size) { i(a[it]) }, abstract) + + private fun assertDelta(context: BaseOnlyAccess, pattern: BaseOnlyAccess, expected: BaseOnlyAccess) { + val m = ai.matchPrefix(context, pattern) + assertTrue(m.hasSuffix, "expected a delta for context=$context pattern=$pattern") + assertFalse(m.emptyDelta) + assertEquals(expected, m.suffix, "wrong delta for context=$context pattern=$pattern") + } + private fun assertIdentity(a: BaseOnlyAccess) { + val m = ai.matchPrefix(a, a) + assertTrue(m.emptyDelta); assertFalse(m.hasSuffix) + } + private fun assertNoMatch(context: BaseOnlyAccess, pattern: BaseOnlyAccess) { + val m = ai.matchPrefix(context, pattern) + assertFalse(m.hasSuffix, "expected NO_MATCH for context=$context pattern=$pattern") + assertFalse(m.emptyDelta) + } + + // canonical shapes + private val apStatic get() = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) // (-2,-1,-1) + private val apFieldNoStat get() = ai.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) // (-1,-2,-1) + private val apFieldStat get() = ai.abstractAt(i(stat), NO_ACCESSOR, 1) // (s1,-2,-1) + private val apSuffixEmpty get() = ai.abstractEmpty // (-1,-1,-2) + private val apSuffixStat get() = ai.abstractAt(i(stat), NO_ACCESSOR, 2) // (s1,-1,-2) + private val apSuffixField get() = ai.abstractAt(NO_ACCESSOR, i(field), 2) // (-1,f1,-2) + + @Test fun `AP@static covers static-carrying, delta is the whole static fact`() { + assertDelta(chain(stat, mark), apStatic, chain(stat, mark)) // (s,-1,t) -> whole + assertIdentity(apStatic) + } + @Test fun `AP@static does NOT cover a static-less fact`() { + assertNoMatch(chain(mark), apStatic) // (-1,-1,t) + assertNoMatch(chain(field, mark), apStatic) // (-1,f,t) + } + @Test fun `AP@field with static committed yields field-leading delta`() { + assertDelta(chain(stat, field, mark), apFieldStat, chain(field, mark)) // (s,f,t) -> (-1,f,t) + assertIdentity(apFieldStat) + } + @Test fun `AP@field with static committed rejects wrong or missing static`() { + assertNoMatch(chain(field, mark), apFieldStat) // no static + } + @Test fun `AP@field no-static yields field-leading delta`() { + assertDelta(chain(field, mark), apFieldNoStat, chain(field, mark)) // (-1,f,t) -> (-1,f,t) + assertNoMatch(chain(stat, field, mark), apFieldNoStat) // known-empty static strict + } + @Test fun `AP@suffix empty yields terminal-leading delta and covers a structural fact through Any`() { + assertDelta(chain(mark), apSuffixEmpty, chain(mark)) // (-1,-1,t) -> (-1,-1,t) + assertNoMatch(chain(stat, mark), apSuffixEmpty) // known-empty static strict + assertDelta(chain(field, mark), apSuffixEmpty, chain(mark)) // virtual Any consumes the field + assertIdentity(apSuffixEmpty) + } + @Test fun `AP@suffix with static committed yields terminal-leading delta`() { + assertDelta(chain(stat, mark), apSuffixStat, chain(mark)) // (s,-1,t) -> (-1,-1,t) + assertNoMatch(chain(mark), apSuffixStat) // missing static + } + @Test fun `AP@suffix with field committed yields terminal-leading delta`() { + assertDelta(chain(field, mark), apSuffixField, chain(mark)) // (-1,f,t) -> (-1,-1,t) + assertNoMatch(chain(mark), apSuffixField) // missing field + } + @Test fun `concrete pattern never yields a delta`() { + assertNoMatch(chain(mark), chain(FinalAccessor)) // initial has no AP + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaTest.kt new file mode 100644 index 000000000..cc52d9350 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaTest.kt @@ -0,0 +1,329 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils +import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.ir.api.common.CommonType +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class BaseOnlyDeltaTest { + private val arg0 = AccessPathBase.Argument(0) + private val field = FieldAccessor("A", "f", "B") + private val mark = TaintMarkAccessor("m") + private val mark2 = TaintMarkAccessor("m2") + + private fun mgr(fieldSensitive: Boolean = false) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.finalOf(vararg accessors: Accessor): BaseOnlyFinalFactAp { + var f: FinalFactAp = createFinalAp(arg0, ExclusionSet.Empty) + accessors.reversed().forEach { f = f.prependAccessor(it) } + return f as BaseOnlyFinalFactAp + } + + private fun BaseOnlyApManager.abstractInitialOf(vararg accessors: Accessor): InitialFactAp { + var f = mostAbstractInitialAp(arg0) + accessors.reversed().forEach { f = f.prependAccessor(it) } + return f + } + + @Test + fun `delta yields the suffix beyond the initial prefix`() { + val m = mgr() + val f = m.finalOf(AnyAccessor, mark) + val i = m.abstractInitialOf(AnyAccessor) + val deltas = f.delta(i) + assertEquals(1, deltas.size) + val d = deltas.single() + assertFalse(d.isEmpty) + assertTrue(d.startsWithAccessor(mark)) + } + + @Test + fun `concat re-appends the semantic delta at the supplied abstract root`() { + val m = mgr() + val f = m.finalOf(AnyAccessor, mark) + val prefix = m.mostAbstractFinalAp(arg0) + val d = f.delta(m.abstractInitialOf(AnyAccessor)).single() + val reconstructed = prefix.concat(FactTypeChecker.Dummy, d) + assertEquals(m.finalOf(mark), reconstructed) + } + + @Test + fun `equal fact and prefix yield empty delta`() { + val m = mgr() + val f = m.finalOf(mark) + val i = m.abstractInitialOf(mark) + assertTrue(f.hasEmptyDelta(i)) + assertTrue(f.delta(i).any { it.isEmpty }) + } + + @Test + fun `split delta preserves concrete field between field and suffix abstractions`() { + val m = mgr(fieldSensitive = true) + val callerFact = m.abstractInitialOf(field, AnyAccessor) as BaseOnlyInitialFactAp + val fieldAbstractAccess = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) + val summaryFinal = BaseOnlyFinalFactAp(m, arg0, fieldAbstractAccess, ExclusionSet.Empty) + + val (matched, delta) = callerFact.splitDelta(summaryFinal).single() + assertEquals(fieldAbstractAccess, (matched as BaseOnlyInitialFactAp).access) + assertTrue(delta is BaseOnlyNodeInitialDelta) + assertEquals(callerFact.access, delta.access) + + val mappedSummaryInitial = BaseOnlyInitialFactAp(m, arg0, fieldAbstractAccess, ExclusionSet.Empty) + assertEquals(callerFact, mappedSummaryInitial.concat(delta)) + } + + @Test + fun `split delta preserves suffix abstraction after a field abstract summary`() { + val m = mgr(fieldSensitive = true) + val callerFact = m.abstractInitialOf(AnyAccessor) as BaseOnlyInitialFactAp + val fieldAbstractAccess = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) + val summaryFinal = BaseOnlyFinalFactAp(m, arg0, fieldAbstractAccess, ExclusionSet.Empty) + + val (matched, delta) = callerFact.splitDelta(summaryFinal).single() + assertEquals(fieldAbstractAccess, (matched as BaseOnlyInitialFactAp).access) + assertTrue(delta is BaseOnlyNodeInitialDelta) + assertEquals(callerFact.access, delta.access) + + val mappedSummaryInitial = BaseOnlyInitialFactAp(m, arg0, fieldAbstractAccess, ExclusionSet.Empty) + assertEquals(callerFact, mappedSummaryInitial.concat(delta)) + } + + @Test + fun `split delta retains implicit Any continuation after structural alignment`() { + val m = mgr(fieldSensitive = true) + var callerFact = m.createFinalInitialAp(arg0, ExclusionSet.Empty) + callerFact = callerFact.prependAccessor(mark) + callerFact = callerFact.prependAccessor(field) + val summaryAccess = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, m.interner.index(field), 2) + + val oneCompactBranchExcluded = BaseOnlyFinalFactAp( + m, + arg0, + summaryAccess, + ExclusionSet.Empty.add(mark), + ) + val directRetained = callerFact.splitDelta(oneCompactBranchExcluded).single().second + as BaseOnlyNodeInitialDelta + assertEquals(BaseOnlyValueAccessorState.Normal, directRetained.access.valueAccessorState) + + val allBranchesExcluded = BaseOnlyFinalFactAp( + m, + arg0, + summaryAccess, + ExclusionSet.Universe, + ) + assertTrue(callerFact.splitDelta(allBranchesExcluded).isEmpty()) + } + + @Test + fun `value fact against abstract prefix yields a value delta not empty`() { + val m = mgr() + val f = m.finalOf() // arg0.$ (value itself) + val i = m.abstractInitialOf(AnyAccessor) // arg0.* + val deltas = f.delta(i) + assertTrue(deltas.none { it.isEmpty }) + val d = deltas.single() + // concatenating onto an abstract result must stay a value (.$), not widen to .* + val result = m.mostAbstractFinalAp(arg0).concat(FactTypeChecker.Dummy, d) + assertEquals(m.finalOf(), result) + } + + @Test + fun `AP@suffix Any prefix matches a retained concrete field`() { + val m = mgr(fieldSensitive = true) + val f = m.finalOf(field, AnyAccessor, mark) + assertTrue(f.delta(m.abstractInitialOf(AnyAccessor)).isNotEmpty()) + val d = f.delta(m.abstractInitialOf(field, AnyAccessor)).single() + assertFalse(d.isEmpty) + } + + @Test + fun `summary application produces a refinement for a non-empty delta`() { + val m = mgr() + val f = m.finalOf(AnyAccessor, mark) + val i = m.abstractInitialOf(AnyAccessor) + val results = MethodSummaryEdgeApplicationUtils.tryApplySummaryEdge(f, i) + assertEquals(1, results.size) + assertTrue(results.single() is SummaryEdgeApplication.SummaryApRefinement) + } + + @Test + fun `summary application produces an exclusion refinement for an empty delta`() { + val m = mgr() + val f = m.finalOf(mark) + val i = m.abstractInitialOf(mark) + val results = MethodSummaryEdgeApplicationUtils.tryApplySummaryEdge(f, i) + assertTrue(results.any { it is SummaryEdgeApplication.SummaryExclusionRefinement }) + } + + @Test + fun `equalTo matches a final fact against its final-accessor initial`() { + val m = mgr() + val f = m.finalOf(mark) + var i = m.createFinalInitialAp(arg0, ExclusionSet.Empty) + i = i.prependAccessor(mark) + assertTrue(f.equalTo(i)) + } + + @Test + fun `append does not stack a second terminal after a taint mark`() { + val m = mgr() + val terminated = m.finalOf(AnyAccessor, mark).access + val extra = m.finalOf(mark2).access + val appended = BaseOnlyAccessOps.append(terminated, extra)!! + assertEquals(m.finalOf(AnyAccessor, mark).access, appended) + var markCount = 0 + var hasMark2 = false + appended.forEachAccessorIdx { + if (it == m.interner.index(mark)) markCount++ + if (it == m.interner.index(mark2)) hasMark2 = true + } + assertEquals(1, markCount) + assertFalse(hasMark2) + } + + @Test + fun `concat of a non-empty delta onto a closed mark fact is rejected`() { + val m = mgr() + val terminated = m.finalOf(AnyAccessor, mark) + val delta = BaseOnlyNodeFinalDelta(m, m.finalOf(mark2).access) + assertNull(terminated.concat(FactTypeChecker.Dummy, delta)) + } + + @Test + fun `concat of a non-empty delta onto a closed value fact is rejected`() { + val m = mgr() + val delta = BaseOnlyNodeFinalDelta(m, m.finalOf(mark).access) + assertNull(m.finalOf().concat(FactTypeChecker.Dummy, delta)) + } + + @Test + fun `concat grafts a terminal onto an abstract receiver`() { + val m = mgr() + val delta = BaseOnlyNodeFinalDelta(m, m.finalOf(mark).access) + assertEquals(m.finalOf(mark), m.mostAbstractFinalAp(arg0).concat(FactTypeChecker.Dummy, delta)) + } + + @Test + fun `contains holds for an exact match and not for a proper prefix`() { + val m = mgr() + val f = m.finalOf(AnyAccessor, mark) + assertTrue(f.contains(m.abstractInitialOf(AnyAccessor, mark))) + assertFalse(f.contains(m.abstractInitialOf(AnyAccessor))) + } + + @Test + fun `final delta checks base before matching`() { + val m = mgr() + val initial = BaseOnlyInitialFactAp( + m, + AccessPathBase.Argument(1), + m.finalOf(mark).access, + ExclusionSet.Empty, + ) + assertTrue(m.finalOf(mark).delta(initial).isEmpty()) + } + + @Test + fun `final concat uses path filter rather than compatibility filter`() { + val m = mgr() + val checker = object : FactTypeChecker { + override fun filterFactByLocalType(actualType: CommonType?, factAp: FinalFactAp): FinalFactAp? = factAp + override fun accessPathFilter(accessPath: List): FactTypeChecker.FactApFilter = + FactTypeChecker.AlwaysAcceptFilter + override fun accessPathCompatibilityFilter(accessPath: List): FactTypeChecker.FactCompatibilityFilter = + object : FactTypeChecker.FactCompatibilityFilter { + override fun check(accessor: Accessor): FactTypeChecker.CompatibilityFilterResult = + if (accessor == mark) FactTypeChecker.CompatibilityFilterResult.NotCompatible + else FactTypeChecker.CompatibilityFilterResult.Compatible + } + } + val delta = BaseOnlyNodeFinalDelta(m, m.finalOf(mark).access) + assertEquals(m.finalOf(mark), m.mostAbstractFinalAp(arg0).concat(checker, delta)) + } + + @Test + fun `final concat advances the supplied path filter through the delta`() { + val m = mgr(fieldSensitive = true) + val seenPrefixes = mutableListOf>() + val rejectAfterMark = object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + if (accessor == FinalAccessor) FactTypeChecker.FilterResult.Reject + else FactTypeChecker.FilterResult.Accept + } + val checker = object : FactTypeChecker { + override fun filterFactByLocalType(actualType: CommonType?, factAp: FinalFactAp): FinalFactAp? = factAp + override fun accessPathFilter(accessPath: List): FactTypeChecker.FactApFilter { + seenPrefixes += accessPath + return object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + if (accessor == mark) FactTypeChecker.FilterResult.FilterNext(rejectAfterMark) + else FactTypeChecker.FilterResult.Reject + } + } + override fun accessPathCompatibilityFilter(accessPath: List): FactTypeChecker.FactCompatibilityFilter = + FactTypeChecker.AlwaysCompatibleFilter + } + val receiver = BaseOnlyFinalFactAp( + m, + arg0, + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, m.interner.index(field), 2), + ExclusionSet.Empty, + ) + val delta = BaseOnlyNodeFinalDelta(m, m.finalOf(mark).access) + + assertNull(receiver.concat(checker, delta)) + assertEquals(listOf(field), seenPrefixes.single()) + } + + @Test + fun `abstractOnly preserves existing AP position and collapsed facts are transient until rebase`() { + val m = mgr(fieldSensitive = true) + val fact = m.finalOf(field, AnyAccessor, mark) + assertEquals(m.mostAbstractFinalAp(arg0), fact.abstractOnly()) + + for (access in listOf( + packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR), + packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR), + )) { + val positioned = BaseOnlyFinalFactAp(m, arg0, access, ExclusionSet.Empty) + assertEquals(positioned, positioned.abstractOnly()) + } + + val rootTransient = fact.abstractOnly().removeAbstraction() + assertNotNull(rootTransient) + assertFalse(rootTransient.isAbstract()) + assertEquals(fact.abstractOnly(), rootTransient.rebase(arg0)) + + val collapsed = packBaseOnlyAccess(NO_ACCESSOR, m.interner.index(field), COLLAPSED_MARK) + val transient = BaseOnlyFinalFactAp(m, arg0, collapsed, ExclusionSet.Empty) + assertFalse(transient.isAbstract()) + assertEquals( + BaseOnlyFinalFactAp( + m, + arg0, + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, m.interner.index(field), 2), + ExclusionSet.Empty, + ), + transient.rebase(arg0), + ) + assertTrue(collapsed.isCollapsed) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FSummaryStorageLawTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FSummaryStorageLawTest.kt new file mode 100644 index 000000000..7dc48fe5a --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FSummaryStorageLawTest.kt @@ -0,0 +1,1043 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Edge +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactToFactEdgeBuilder +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.ap.ifds.access.common.CommonF2FSummary +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlyF2FSummaryStorageLawTest { + private val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + summaryStorageFieldGeneralizationEnabled = true, + ) + private val entryPoint by lazy { MethodEntryPoint(EmptyMethodContext, inst) } + private val exA = ExclusionSet.Concrete(TaintMarkAccessor("excluded-a")) + private val exB = ExclusionSet.Concrete(TaintMarkAccessor("excluded-b")) + private val exC = ExclusionSet.Concrete(TaintMarkAccessor("excluded-c")) + + @Test + fun `normalized alias emits no delta and reads the primary exclusion`() { + val summaries = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager) + val initial = packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR) + val normalized = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK) + val final = packBaseOnlyAccess(NO_ACCESSOR, field("field"), ABSTRACT_MARK) + + val firstDelta = mutableListOf() + summaries.add(listOf(edge(initial, final, exA)), firstDelta) + assertEquals(listOf(exA), firstDelta.map { it.record().exclusion }) + + val secondDelta = mutableListOf() + summaries.add(listOf(edge(initial, final, exB)), secondDelta) + assertEquals(listOf(ExclusionSet.Empty), secondDelta.map { it.record().exclusion }) + + manager.enableTraceResolutionMode() + val records = summaries.records() + assertEquals(2, records.size) + assertEquals( + setOf(initial, normalized), + records.mapTo(hashSetOf()) { it.initial }, + "the alias is a query view, not a second insertion delta", + ) + assertTrue(records.all { it.exclusion == ExclusionSet.Empty }) + } + + @Test + fun `normalized alias and exact primary merge as one logical view`() { + val summaries = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager) + val original = packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR) + val normalized = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK) + val final = packBaseOnlyAccess(NO_ACCESSOR, field("field-2"), ABSTRACT_MARK) + val added = mutableListOf() + + summaries.add(listOf(edge(original, final, exA), edge(normalized, final, exB)), added) + assertEquals(2, added.size, "both primary aggregates contribute insertion deltas") + + manager.enableTraceResolutionMode() + val records = summaries.records() + assertEquals(2, records.size, "the alias must not duplicate the exact primary view") + assertEquals(exA, records.single { it.initial == original }.exclusion) + assertEquals( + ExclusionSet.Empty, + records.single { it.initial == normalized }.exclusion, + "alternative alias/primary exclusions merge by intersection", + ) + } + + @Test + fun `repeated same-key updates in one batch emit one committed aggregate`() { + val summaries = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager) + val field = field("batch") + val initial = packBaseOnlyAccess(NO_ACCESSOR, field, ABSTRACT_MARK) + val final = packBaseOnlyAccess(NO_ACCESSOR, field, mark("batch-final")) + val added = mutableListOf() + + summaries.add(listOf(edge(initial, final, exA), edge(initial, final, exB)), added) + + assertEquals(1, added.size) + assertEquals(ExclusionSet.Empty, added.single().record().exclusion) + assertEquals(listOf(ExclusionSet.Empty), summaries.records().map { it.exclusion }) + } + + @Test + fun `rejected transient summary has no observable partition or delta`() { + val summaries = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager) + val initial = packBaseOnlyAccess(NO_ACCESSOR, field("valid-initial"), ABSTRACT_MARK) + val collapsed = packBaseOnlyAccess(NO_ACCESSOR, field("transient-final"), COLLAPSED_MARK) + val invalid = edge(initial, collapsed, exA) + val rejectedDelta = mutableListOf() + + summaries.add(listOf(invalid), rejectedDelta) + assertTrue(rejectedDelta.isEmpty()) + assertTrue(summaries.records().isEmpty()) + + val final = packBaseOnlyAccess(NO_ACCESSOR, field("valid-final"), mark("valid-mark")) + val acceptedDelta = mutableListOf() + summaries.add(listOf(edge(initial, final, exA)), acceptedDelta) + assertEquals(1, acceptedDelta.size) + assertEquals(1, summaries.records().size) + } + + @Test + fun `different finals keep independent exclusions in either insertion order`() { + val field = field("aggregate") + val initial = packBaseOnlyAccess(NO_ACCESSOR, field, ABSTRACT_MARK) + val finalA = packBaseOnlyAccess(NO_ACCESSOR, field, mark("aggregate-a")) + val finalB = packBaseOnlyAccess(NO_ACCESSOR, field, mark("aggregate-b")) + + fun run(edges: List): Set { + val summaries = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager) + val delta = mutableListOf() + summaries.add(edges, delta) + assertEquals(2, delta.size) + return summaries.records().toSet() + } + + val forward = run(listOf(edge(initial, finalA, exA), edge(initial, finalB, exB))) + val reverse = run(listOf(edge(initial, finalB, exB), edge(initial, finalA, exA))) + + assertEquals(forward, reverse) + assertEquals( + setOf( + Record(initial, finalA, exA), + Record(initial, finalB, exB), + ), + forward, + ) + } + + @Test + fun `identity cross-slot records remain distinct in both insertion orders`() { + val suffix = manager.interner.index(TaintMarkAccessor("identity-suffix")) + val noField = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, suffix) + val fieldAp = packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR) + + fun run(first: BaseOnlyAccess, second: BaseOnlyAccess): Pair, List> { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val delta = mutableListOf>() + storage.add(listOf(storageEdge(first, first), storageEdge(second, second)), delta) + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + return delta.map(::record) to current.map(::record) + } + + for ((first, second) in listOf(noField to fieldAp, fieldAp to noField)) { + val (delta, current) = run(first, second) + assertEquals(setOf(noField, fieldAp), delta.mapTo(hashSetOf()) { it.initial }, "delta order $first then $second") + assertEquals(setOf(noField, fieldAp), current.mapTo(hashSetOf()) { it.initial }, "state order $first then $second") + } + } + + @Test + fun `identity abstraction suppresses only permitted same-slot children in both insertion orders`() { + val markAccessor = TaintMarkAccessor("identity-child") + val suffix = manager.interner.index(markAccessor) + val abstract = ABSTRACT_EMPTY_ACCESS + val normal = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, suffix, BaseOnlyValueAccessorState.Normal) + val value = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, suffix, BaseOnlyValueAccessorState.Value) + + fun run(edges: List>): Set { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val delta = mutableListOf>() + storage.add(edges, delta) + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + assertEquals(current.mapTo(hashSetOf()) { record(it).initial }, delta.mapTo(hashSetOf()) { record(it).initial }) + return current.mapTo(hashSetOf()) { record(it).initial } + } + + val abstractEdge = storageEdge(abstract, abstract, ExclusionSet.Empty) + val normalEdge = storageEdge(normal, normal, ExclusionSet.Empty) + val valueEdge = storageEdge(value, value, ExclusionSet.Empty) + assertEquals(setOf(abstract), run(listOf(normalEdge, valueEdge, abstractEdge))) + assertEquals(setOf(abstract), run(listOf(abstractEdge, normalEdge, valueEdge))) + + val excludingAbstract = storageEdge( + abstract, + abstract, + ExclusionSet.Concrete(markAccessor), + ) + assertEquals( + setOf(abstract, normal, value), + run(listOf(excludingAbstract, normalEdge, valueEdge)), + "an excluded child must remain explicit, including both value-accessor states", + ) + } + + @Test + fun `value accessor states remain distinct summary keys`() { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val suffix = mark("mode-key") + val final = packBaseOnlyAccess(NO_ACCESSOR, field("mode-final"), mark("mode-result")) + val initials = BaseOnlyValueAccessorState.entries.map { state -> + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, suffix, state) + } + storage.add(initials.map { storageEdge(it, final) }, mutableListOf()) + + fun query(pattern: BaseOnlyAccess?): Set { + val result = mutableListOf>() + storage.collectSummariesTo(result, pattern) + return result.mapTo(hashSetOf()) { record(it).initial } + } + + assertEquals(initials.toSet(), query(null)) + val normal = initials[BaseOnlyValueAccessorState.Normal.ordinal] + val value = initials[BaseOnlyValueAccessorState.Value.ordinal] + assertEquals(setOf(normal), query(normal)) + assertEquals(setOf(value), query(value)) + } + + @Test + fun `correlated abstract edge subsumes its concrete specialization`() { + val fieldA = field("subsumption-a") + val fieldB = field("subsumption-b") + val terminal = mark("subsumption-mark") + val broad = BaseOnlySummaryEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, ABSTRACT_MARK), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, ABSTRACT_MARK), + exclusion = ExclusionSet.Empty, + ) + val narrow = BaseOnlySummaryEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, terminal), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, terminal), + exclusion = ExclusionSet.Empty, + ) + + assertTrue(BaseOnlySummaryEdgeOps.subsumes(manager, broad, narrow)) + assertFalse(BaseOnlySummaryEdgeOps.subsumes(manager, narrow, broad)) + } + + @Test + fun `correlated abstract edge does not subsume a different final residual`() { + val fieldA = field("mismatch-a") + val fieldB = field("mismatch-b") + val broad = BaseOnlySummaryEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, ABSTRACT_MARK), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, ABSTRACT_MARK), + exclusion = ExclusionSet.Empty, + ) + val mismatch = BaseOnlySummaryEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, mark("mismatch-in")), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, mark("mismatch-out")), + exclusion = ExclusionSet.Empty, + ) + + assertFalse(BaseOnlySummaryEdgeOps.subsumes(manager, broad, mismatch)) + } + + @Test + fun `same premise with abstract conclusion subsumes a concrete field conclusion`() { + val premise = packBaseOnlyAccess(NO_ACCESSOR, field("premise-field"), ABSTRACT_MARK) + val abstractConclusion = BaseOnlySummaryEdge( + initial = premise, + final = ABSTRACT_EMPTY_ACCESS, + exclusion = ExclusionSet.Empty, + ) + val concreteConclusion = BaseOnlySummaryEdge( + initial = premise, + final = packBaseOnlyAccess(NO_ACCESSOR, field("conclusion-field"), ABSTRACT_MARK), + exclusion = ExclusionSet.Empty, + ) + + assertTrue(BaseOnlySummaryEdgeOps.subsumes(manager, abstractConclusion, concreteConclusion)) + assertFalse(BaseOnlySummaryEdgeOps.subsumes(manager, concreteConclusion, abstractConclusion)) + } + + @Test + fun `summary antichain keeps the abstract conclusion for a shared premise in either order`() { + val premise = packBaseOnlyAccess(NO_ACCESSOR, field("antichain-premise"), ABSTRACT_MARK) + val abstractConclusion = storageEdge( + initial = premise, + final = ABSTRACT_EMPTY_ACCESS, + ) + val concreteConclusion = storageEdge( + initial = premise, + final = packBaseOnlyAccess(NO_ACCESSOR, field("antichain-conclusion"), ABSTRACT_MARK), + ) + + fun run(edges: List>): Set { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + storage.add(edges, mutableListOf()) + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + return current.mapTo(hashSetOf(), ::record) + } + + val expected = setOf(Record(premise, ABSTRACT_EMPTY_ACCESS, ExclusionSet.Empty)) + assertEquals(expected, run(listOf(abstractConclusion, concreteConclusion))) + assertEquals(expected, run(listOf(concreteConclusion, abstractConclusion))) + } + + @Test + fun `same premise conclusion subsumption preserves exclusion ordering`() { + val premise = packBaseOnlyAccess(NO_ACCESSOR, field("exclusion-premise"), ABSTRACT_MARK) + val generalWithExclusion = BaseOnlySummaryEdge( + initial = premise, + final = ABSTRACT_EMPTY_ACCESS, + exclusion = exA, + ) + val specificWithoutExclusion = BaseOnlySummaryEdge( + initial = premise, + final = packBaseOnlyAccess(NO_ACCESSOR, field("exclusion-conclusion"), ABSTRACT_MARK), + exclusion = ExclusionSet.Empty, + ) + val generalWithoutExclusion = generalWithExclusion.copy(exclusion = ExclusionSet.Empty) + val specificWithExclusion = specificWithoutExclusion.copy(exclusion = exA) + + assertFalse(BaseOnlySummaryEdgeOps.subsumes(manager, generalWithExclusion, specificWithoutExclusion)) + assertTrue(BaseOnlySummaryEdgeOps.subsumes(manager, generalWithoutExclusion, specificWithExclusion)) + } + + @Test + fun `excluded residual prevents summary edge subsumption`() { + val fieldA = field("excluded-a") + val fieldB = field("excluded-b") + val markAccessor = TaintMarkAccessor("excluded-residual") + val terminal = manager.interner.index(markAccessor) + val broad = BaseOnlySummaryEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, ABSTRACT_MARK), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, ABSTRACT_MARK), + exclusion = ExclusionSet.Concrete(markAccessor), + ) + val narrow = BaseOnlySummaryEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, terminal), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, terminal), + exclusion = ExclusionSet.Empty, + ) + + assertFalse(BaseOnlySummaryEdgeOps.subsumes(manager, broad, narrow)) + } + + @Test + fun `summary antichain keeps only broad correlated edge in either insertion order`() { + val fieldA = field("antichain-a") + val fieldB = field("antichain-b") + val terminal = mark("antichain-mark") + val broad = storageEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, ABSTRACT_MARK), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, ABSTRACT_MARK), + ) + val narrow = storageEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, terminal), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, terminal), + ) + + fun run(edges: List>): Set { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val delta = mutableListOf>() + storage.add(edges, delta) + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + assertEquals(current.map(::record).toSet(), delta.map(::record).toSet()) + return current.mapTo(hashSetOf(), ::record) + } + + val expected = setOf(Record(broad.initial, broad.final, ExclusionSet.Empty)) + assertEquals(expected, run(listOf(narrow, broad))) + assertEquals(expected, run(listOf(broad, narrow))) + } + + @Test + fun `adding broad edge evicts published narrow edge and adding narrow edge is ignored`() { + val fieldA = field("incremental-a") + val fieldB = field("incremental-b") + val terminal = mark("incremental-mark") + val broad = storageEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, ABSTRACT_MARK), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, ABSTRACT_MARK), + ) + val narrow = storageEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, fieldA, terminal), + final = packBaseOnlyAccess(NO_ACCESSOR, fieldB, terminal), + ) + + val narrowThenBroad = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + narrowThenBroad.add(listOf(narrow), mutableListOf()) + val broadDelta = mutableListOf>() + narrowThenBroad.add(listOf(broad), broadDelta) + assertEquals(setOf(broad.initial), broadDelta.mapTo(hashSetOf()) { record(it).initial }) + val afterEviction = mutableListOf>() + narrowThenBroad.collectSummariesTo(afterEviction, null) + assertEquals(setOf(broad.initial), afterEviction.mapTo(hashSetOf()) { record(it).initial }) + + val broadThenNarrow = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + broadThenNarrow.add(listOf(broad), mutableListOf()) + val ignoredDelta = mutableListOf>() + broadThenNarrow.add(listOf(narrow), ignoredDelta) + assertTrue(ignoredDelta.isEmpty()) + } + + @Test + fun `field generalization has an eight edge budget and monotone deltas`() { + val members = (0 until 18).map { index -> + storageEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, field("budget-$index"), ABSTRACT_MARK), + final = ABSTRACT_EMPTY_ACCESS, + exclusion = if (index % 2 == 0) exA else exB, + ) + } + val representative = Record( + initial = ABSTRACT_EMPTY_ACCESS, + final = ABSTRACT_EMPTY_ACCESS, + exclusion = ExclusionSet.Empty, + ) + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + + val belowBudgetDelta = mutableListOf>() + storage.add(members.take(MAX_FIELD_ENUMERATION_EDGES), belowBudgetDelta) + assertEquals( + members.take(MAX_FIELD_ENUMERATION_EDGES) + .mapTo(hashSetOf()) { Record(it.initial, it.final, it.exclusion) }, + belowBudgetDelta.mapTo(hashSetOf(), ::record), + ) + + val crossingDelta = mutableListOf>() + storage.add(listOf(members[MAX_FIELD_ENUMERATION_EDGES]), crossingDelta) + assertEquals(listOf(representative), crossingDelta.map(::record)) + + val afterCrossing = mutableListOf>() + storage.collectSummariesTo(afterCrossing, null) + assertEquals(listOf(representative), afterCrossing.map(::record)) + assertEquals( + MAX_FIELD_ENUMERATION_EDGES + 1, + belowBudgetDelta.size + crossingDelta.size, + "already published exact deltas cannot be retracted when the group is generalized", + ) + + val absorbed = members[MAX_FIELD_ENUMERATION_EDGES + 1].copy(exclusion = exC) + val absorbedDelta = mutableListOf>() + storage.add(listOf(absorbed), absorbedDelta) + val representativeWithAbsorbedExclusion = representative + assertTrue( + absorbedDelta.isEmpty(), + "a member that does not change the common exclusion emits no new representative", + ) + + val afterAbsorption = mutableListOf>() + storage.collectSummariesTo(afterAbsorption, null) + assertEquals(listOf(representativeWithAbsorbedExclusion), afterAbsorption.map(::record)) + + val repeatedDelta = mutableListOf>() + storage.add(listOf(absorbed), repeatedDelta) + assertTrue(repeatedDelta.isEmpty(), "an unchanged generalized representative emits no delta") + } + + @Test + fun `absorbed member publishes a broader representative when common exclusion shrinks`() { + val commonExclusion = ExclusionSet.Concrete(TaintMarkAccessor("initially-common")) + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val members = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + storageEdge( + initial = packBaseOnlyAccess( + NO_ACCESSOR, + field("shrinking-$index"), + ABSTRACT_MARK, + ), + final = ABSTRACT_EMPTY_ACCESS, + exclusion = commonExclusion, + ) + } + + val crossingDelta = mutableListOf>() + storage.add(members, crossingDelta) + assertEquals( + listOf(Record(ABSTRACT_EMPTY_ACCESS, ABSTRACT_EMPTY_ACCESS, commonExclusion)), + crossingDelta.map(::record), + ) + + val absorbed = storageEdge( + initial = packBaseOnlyAccess( + NO_ACCESSOR, + field("shrinking-absorbed"), + ABSTRACT_MARK, + ), + final = ABSTRACT_EMPTY_ACCESS, + exclusion = ExclusionSet.Empty, + ) + val broaderDelta = mutableListOf>() + storage.add(listOf(absorbed), broaderDelta) + val broader = Record(ABSTRACT_EMPTY_ACCESS, ABSTRACT_EMPTY_ACCESS, ExclusionSet.Empty) + assertEquals(listOf(broader), broaderDelta.map(::record)) + + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + assertEquals(listOf(broader), current.map(::record)) + } + + @Test + fun `field generalization does not erase concrete semantic suffixes`() { + val members = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + storageEdge( + initial = packBaseOnlyAccess( + NO_ACCESSOR, + field("marked-in-$index"), + mark("marked-suffix-$index"), + ), + final = packBaseOnlyAccess( + NO_ACCESSOR, + field("marked-out-$index"), + mark("marked-suffix-$index"), + ), + ) + } + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + storage.add(members, mutableListOf()) + + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + + assertEquals( + members.size, + current.size, + "field by concrete-mark mappings are semantic alternatives, not field enumeration", + ) + } + + @Test + fun `generalized alternatives intersect suffix exclusions`() { + val excludedByOneAlternative = TaintMarkAccessor("excluded-by-one-alternative") + val members = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + storageEdge( + initial = packBaseOnlyAccess( + NO_ACCESSOR, + field("alternative-$index"), + ABSTRACT_MARK, + ), + final = ABSTRACT_EMPTY_ACCESS, + exclusion = if (index == 0) { + ExclusionSet.Concrete(excludedByOneAlternative) + } else { + ExclusionSet.Empty + }, + ) + } + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + storage.add(members, mutableListOf()) + + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + val representative = current.single().apply { + setInitialFactBase(AccessPathBase.This) + setExitFactBase(AccessPathBase.Return) + }.build() + .setEntryPoint(entryPoint) + .setExitStatement(inst) + .build() + + assertEquals(ExclusionSet.Empty, representative.initialFactAp.exclusions) + + val input = BaseOnlyFinalFactAp( + manager, + AccessPathBase.This, + packBaseOnlyAccess( + NO_ACCESSOR, + field("alternative-1"), + manager.interner.index(excludedByOneAlternative), + ), + ExclusionSet.Empty, + ) + assertTrue( + MethodSummaryEdgeApplicationUtils.tryApplySummaryEdge( + input, + representative.initialFactAp, + ).isNotEmpty(), + "an exclusion from one erased premise must not reject a suffix accepted by another", + ) + } + + @Test + fun `generalized exclusion keeps only common suffix accessors`() { + val commonMark = TaintMarkAccessor("common-suffix-exclusion") + val members = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + val structural = FieldAccessor("Owner", "excluded-field-$index", "Value") + storageEdge( + initial = packBaseOnlyAccess( + NO_ACCESSOR, + field("common-exclusion-$index"), + ABSTRACT_MARK, + ), + final = ABSTRACT_EMPTY_ACCESS, + exclusion = ExclusionSet.Empty.add(commonMark).add(structural), + ) + } + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + storage.add(members, mutableListOf()) + + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + + assertEquals( + ExclusionSet.Concrete(commonMark), + record(current.single()).exclusion, + "exclusions for erased fields are meaningless; the common suffix exclusion remains", + ) + } + + @Test + fun `field generalization can be disabled`() { + val exactManager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + summaryStorageFieldGeneralizationEnabled = false, + ) + val members = (0 until MAX_FIELD_ENUMERATION_EDGES + 2).map { index -> + storageEdge( + initial = packBaseOnlyAccess( + NO_ACCESSOR, + exactManager.interner.index(FieldAccessor("Owner", "exact-$index", "Value")), + ABSTRACT_MARK, + ), + final = ABSTRACT_EMPTY_ACCESS, + exclusion = if (index % 2 == 0) exA else exB, + ) + } + val expected = members.mapTo(hashSetOf()) { + Record(it.initial, it.final, it.exclusion) + } + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, exactManager).createStorage() + val delta = mutableListOf>() + + storage.add(members, delta) + exactManager.enableTraceResolutionMode() + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + + assertEquals(expected, delta.mapTo(hashSetOf(), ::record)) + assertEquals(expected, current.mapTo(hashSetOf(), ::record)) + assertFalse(current.map(::record).any { + it.initial == ABSTRACT_EMPTY_ACCESS && it.final == ABSTRACT_EMPTY_ACCESS + }) + } + + @Test + fun `field generalization is invariant under insertion order`() { + val members = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + storageEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, field("order-$index"), ABSTRACT_MARK), + final = ABSTRACT_EMPTY_ACCESS, + exclusion = if (index % 2 == 0) exA else exB, + ) + } + val representative = Record( + initial = ABSTRACT_EMPTY_ACCESS, + final = ABSTRACT_EMPTY_ACCESS, + exclusion = ExclusionSet.Empty, + ) + val orders = buildList { + add(members) + add(members.reversed()) + for (shift in listOf(1, 5, 11)) { + add(members.drop(shift) + members.take(shift)) + } + } + + orders.forEachIndexed { orderIndex, order -> + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val delta = mutableListOf>() + storage.add(order, delta) + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + + assertEquals(listOf(representative), delta.map(::record), "delta for order $orderIndex") + assertEquals(listOf(representative), current.map(::record), "state for order $orderIndex") + } + } + + @Test + fun `static semantic and value dimensions are excluded from field generalization`() { + fun assertRetained( + scenario: String, + edges: List>, + ) { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val delta = mutableListOf>() + storage.add(edges, delta) + val current = mutableListOf>() + storage.collectSummariesTo(current, null) + val expected = edges.mapTo(hashSetOf()) { Record(it.initial, it.final, it.exclusion) } + assertEquals(expected, delta.mapTo(hashSetOf(), ::record), "$scenario delta") + assertEquals(expected, current.mapTo(hashSetOf(), ::record), "$scenario state") + } + + val staticInitial = static("non-generalized-initial-static") + assertRetained( + "initial static", + (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + storageEdge( + packBaseOnlyAccess(staticInitial, field("static-in-$index"), ABSTRACT_MARK), + ABSTRACT_EMPTY_ACCESS, + ) + }, + ) + + val staticFinal = static("non-generalized-final-static") + assertRetained( + "final static", + (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + storageEdge( + packBaseOnlyAccess(NO_ACCESSOR, field("static-out-$index"), ABSTRACT_MARK), + packBaseOnlyAccess(staticFinal, NO_ACCESSOR, ABSTRACT_MARK), + ) + }, + ) + + val initialSemantic = mark("non-generalized-initial-semantic") + assertRetained( + "initial semantic", + (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + storageEdge( + packBaseOnlyAccess(NO_ACCESSOR, field("semantic-in-$index"), initialSemantic), + ABSTRACT_EMPTY_ACCESS, + ) + }, + ) + + val finalSemantic = mark("non-generalized-final-semantic") + assertRetained( + "final semantic and value mode", + (0..MAX_FIELD_ENUMERATION_EDGES).flatMap { index -> + val initial = packBaseOnlyAccess(NO_ACCESSOR, field("semantic-out-$index"), ABSTRACT_MARK) + BaseOnlyValueAccessorState.entries.map { state -> + storageEdge( + initial, + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, finalSemantic, state), + ) + } + }, + ) + } + + @Test + fun `pattern filtering finds the generalized edge for every removed premise`() { + val structuralAccessors = listOf(ELEMENT_ACCESSOR_IDX) + + (0 until MAX_FIELD_ENUMERATION_EDGES).map { index -> field("pattern-generalized-$index") } + val members = structuralAccessors.map { accessor -> + storageEdge( + initial = packBaseOnlyAccess(NO_ACCESSOR, accessor, ABSTRACT_MARK), + final = if (accessor == ELEMENT_ACCESSOR_IDX) { + packBaseOnlyAccess(NO_ACCESSOR, ELEMENT_ACCESSOR_IDX, ABSTRACT_MARK) + } else { + ABSTRACT_EMPTY_ACCESS + }, + ) + } + val representative = Record( + initial = ABSTRACT_EMPTY_ACCESS, + final = ABSTRACT_EMPTY_ACCESS, + exclusion = ExclusionSet.Empty, + ) + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + storage.add(members, mutableListOf()) + + members.forEach { member -> + val queried = mutableListOf>() + storage.collectSummariesTo(queried, member.initial) + assertEquals( + listOf(representative), + queried.map(::record), + "removed premise ${member.initial} must select its generalized representative", + ) + } + + manager.enableTraceResolutionMode() + val all = mutableListOf>() + storage.collectSummariesTo(all, null) + assertEquals(listOf(representative), all.map(::record), "normalized views must not duplicate the representative") + } + + @Test + fun `concurrent first-leaf publication never exposes synthetic Universe exclusion`() { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val failures = ConcurrentLinkedQueue() + val started = CountDownLatch(1) + val finished = AtomicBoolean(false) + val executor = Executors.newFixedThreadPool(4) + val exclusion = ExclusionSet.Concrete(TaintMarkAccessor("real-exclusion")) + val count = 2_000 + + executor.submit { + try { + started.countDown() + repeat(count) { index -> + val suffix = manager.interner.index(TaintMarkAccessor("leaf-$index")) + val access = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, suffix) + storage.add(listOf(storageEdge(access, access, exclusion)), mutableListOf()) + } + } catch (t: Throwable) { + failures += t + } finally { + finished.set(true) + } + } + repeat(3) { + executor.submit { + try { + started.await() + while (!finished.get()) { + val observed = mutableListOf>() + storage.collectSummariesTo(observed, null) + observed.forEach { builder -> + assertFalse(record(builder).exclusion is ExclusionSet.Universe) + } + } + } catch (t: Throwable) { + failures += t + } + } + } + + executor.shutdown() + assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS)) + assertTrue(failures.isEmpty(), failures.joinToString("\n")) + + val eventual = mutableListOf>() + storage.collectSummariesTo(eventual, null) + assertEquals(count, eventual.size) + assertTrue(eventual.all { record(it).exclusion == exclusion }) + } + + @Test + fun `concurrent publication preserves each final exclusion`() { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val initial = packBaseOnlyAccess(NO_ACCESSOR, field("publication"), ABSTRACT_MARK) + val firstFinal = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, mark("publication-first")) + storage.add(listOf(storageEdge(initial, firstFinal, exA)), mutableListOf()) + + val failures = ConcurrentLinkedQueue() + val started = CountDownLatch(1) + val finished = AtomicBoolean(false) + val executor = Executors.newFixedThreadPool(4) + val count = 2_000 + + executor.submit { + try { + started.countDown() + repeat(count) { index -> + val final = packBaseOnlyAccess( + NO_ACCESSOR, + NO_ACCESSOR, + manager.interner.index(TaintMarkAccessor("publication-$index")), + ) + storage.add(listOf(storageEdge(initial, final, exB)), mutableListOf()) + } + } catch (t: Throwable) { + failures += t + } finally { + finished.set(true) + } + } + repeat(3) { + executor.submit { + try { + started.await() + while (!finished.get()) { + val observed = mutableListOf>() + storage.collectSummariesTo(observed, null) + observed.map(::record).forEach { record -> + assertEquals( + if (record.final == firstFinal) exA else exB, + record.exclusion, + "a final was observed with another edge's exclusion", + ) + } + } + } catch (t: Throwable) { + failures += t + } + } + } + + executor.shutdown() + assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS)) + assertTrue(failures.isEmpty(), failures.joinToString("\n")) + + val eventual = mutableListOf>() + storage.collectSummariesTo(eventual, null) + assertEquals(count + 1, eventual.size) + eventual.map(::record).forEach { record -> + assertEquals(if (record.final == firstFinal) exA else exB, record.exclusion) + } + } + + @Test + fun `patterned query equals a scan-and-predicate reference`() { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val fieldA = field("query-a") + val fieldB = field("query-b") + val static = static("query-static") + val initials = listOf( + packBaseOnlyAccess(NO_ACCESSOR, fieldA, ABSTRACT_MARK), + packBaseOnlyAccess(NO_ACCESSOR, fieldB, ABSTRACT_MARK), + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK), + packBaseOnlyAccess(static, fieldA, ABSTRACT_MARK), + ) + val inserted = buildList { + initials.forEachIndexed { index, initial -> + add(storageEdge(initial, packBaseOnlyAccess(initial.staticIdx, initial.fieldIdx, mark("query-a-$index")), exA)) + add(storageEdge(initial, packBaseOnlyAccess(initial.staticIdx, initial.fieldIdx, mark("query-b-$index")), exB)) + } + } + storage.add(inserted, mutableListOf()) + + val all = mutableListOf>() + storage.collectSummariesTo(all, null) + val scan = all.map(::record) + val patterns = initials + listOf( + packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR), + packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR), + ) + + for (pattern in patterns) { + val expected = scan.filter { baseOnlySummaryInitialMatches(pattern, it.initial) }.toSet() + val queried = mutableListOf>() + storage.collectSummariesTo(queried, pattern) + assertEquals(expected, queried.map(::record).toSet(), "pattern=$pattern") + } + } + + @Test + fun `final-pattern query selects only overlapping finals after index promotion`() { + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager).createStorage() + val initial = packBaseOnlyAccess(NO_ACCESSOR, field("final-query-initial"), ABSTRACT_MARK) + val finals = List(96) { index -> + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, mark("final-query-$index")) + } + storage.add(finals.map { final -> storageEdge(initial, final) }, mutableListOf()) + + val queried = mutableListOf>() + storage.collectSummariesByFinalTo(queried, finals[73]) + + assertEquals( + setOf(Record(initial, finals[73], ExclusionSet.Empty)), + queried.map(::record).toSet(), + ) + } + + private fun edge(initial: BaseOnlyAccess, final: BaseOnlyAccess, exclusion: ExclusionSet): Edge.FactToFact = + Edge.FactToFact( + entryPoint, + BaseOnlyInitialFactAp(manager, AccessPathBase.This, initial, exclusion), + inst, + BaseOnlyFinalFactAp(manager, AccessPathBase.Return, final, exclusion), + ) + + private fun field(name: String): Int = + manager.interner.index(FieldAccessor("C", name, "T")) + + private fun mark(name: String): Int = + manager.interner.index(TaintMarkAccessor(name)) + + private fun static(name: String): Int = + manager.interner.index(ClassStaticAccessor(name)) + + private fun storageEdge( + initial: BaseOnlyAccess, + final: BaseOnlyAccess, + exclusion: ExclusionSet = ExclusionSet.Empty, + ) = CommonF2FSummary.StorageEdge(initial, final, exclusion) + + private fun MethodInitialToFinalBaseOnlyApSummariesStorage.records(): List { + val builders = mutableListOf() + filterEdgesTo(builders, initialFactPattern = null, finalFactBase = AccessPathBase.Return) + return builders.map { it.record() } + } + + private fun FactToFactEdgeBuilder.record(): Record { + val edge = setEntryPoint(entryPoint).build() + return Record( + (edge.initialFactAp as BaseOnlyInitialFactAp).access, + (edge.factAp as BaseOnlyFinalFactAp).access, + edge.initialFactAp.exclusions, + ) + } + + private fun record( + builder: CommonF2FSummary.F2FBBuilder, + ): Record { + val edge = builder + .setInitialFactBase(AccessPathBase.This) + .setExitFactBase(AccessPathBase.Return) + .build() + .setEntryPoint(entryPoint) + .setExitStatement(inst) + .build() + return Record( + (edge.initialFactAp as BaseOnlyInitialFactAp).access, + (edge.factAp as BaseOnlyFinalFactAp).access, + edge.initialFactAp.exclusions, + ) + } + + private data class Record( + val initial: BaseOnlyAccess, + val final: BaseOnlyAccess, + val exclusion: ExclusionSet, + ) + + private val method: CommonMethod = object : CommonMethod { + override val name: String = "baseOnlyF2FStorageLaws" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "java.lang.Object" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + + private val inst: CommonInst = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod get() = this@BaseOnlyF2FSummaryStorageLawTest.method + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactOpsTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactOpsTest.kt new file mode 100644 index 000000000..c1de661ac --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactOpsTest.kt @@ -0,0 +1,197 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class BaseOnlyFactOpsTest { + private val arg0 = AccessPathBase.Argument(0) + private val field = FieldAccessor("A", "f", "B") + private val field2 = FieldAccessor("A", "g", "B") + private val mark = TaintMarkAccessor("m") + private val stat = ClassStaticAccessor("T") + private val typeInfo = TypeInfoAccessor("pkg.fn") + + private fun mgr(fieldSensitive: Boolean) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.finalOf(vararg accessors: Accessor): BaseOnlyFinalFactAp { + var f = createFinalAp(arg0, ExclusionSet.Empty) as BaseOnlyFinalFactAp + accessors.reversed().forEach { f = f.prependAccessor(it) as BaseOnlyFinalFactAp } + return f + } + + @Test + fun `md0 prepend field is absorbed`() { + val m = mgr(false) + val argMark = m.finalOf(AnyAccessor, mark) + assertEquals(argMark, argMark.prependAccessor(field)) + } + + @Test + fun `md0 read field returns self`() { + val m = mgr(false) + val argMark = m.finalOf(AnyAccessor, mark) + assertEquals(argMark, argMark.readAccessor(field)) + } + + @Test + fun `md0 starts with field is true`() { + val m = mgr(false) + val argMark = m.finalOf(AnyAccessor, mark) + assertTrue(argMark.startsWithAccessor(field)) + } + + @Test + fun `md1 prepend field is kept before any`() { + val m = mgr(true) + val argMark = m.finalOf(AnyAccessor, mark) + assertEquals(m.finalOf(field, AnyAccessor, mark), argMark.prependAccessor(field)) + } + + @Test + fun `md1 second field replaces first`() { + val m = mgr(true) + val argFieldMark = m.finalOf(field, AnyAccessor, mark) + assertEquals(m.finalOf(field2, AnyAccessor, mark), argFieldMark.prependAccessor(field2)) + } + + @Test + fun `md1 read matching field consumes it`() { + val m = mgr(true) + val argFieldMark = m.finalOf(field, AnyAccessor, mark) + assertEquals(m.finalOf(AnyAccessor, mark), argFieldMark.readAccessor(field)) + } + + @Test + fun `md1 read non matching field is null`() { + val m = mgr(true) + val argFieldMark = m.finalOf(field, AnyAccessor, mark) + assertNull(argFieldMark.readAccessor(field2)) + } + + @Test + fun `plain semantic fact has an implicit structural branch`() { + val m = mgr(false) + val argMark = m.finalOf(mark) + assertTrue(argMark.startsWithAccessor(field)) + assertTrue(argMark.startsWithAccessor(mark)) + assertEquals(argMark, argMark.readAccessor(field)) + } + + @Test + fun `a concrete clear does not consume the implicit Any branch`() { + val m = mgr(false) + val argMark = m.finalOf(AnyAccessor, mark) + assertEquals(argMark, argMark.clearAccessor(field)) + } + + @Test + fun `start accessors expose any and the head for a semantic mark`() { + val m = mgr(false) + assertEquals( + setOf(AnyAccessor, mark), + m.finalOf(AnyAccessor, mark).getStartAccessors(), + ) + assertEquals( + setOf(AnyAccessor, mark), + m.finalOf(mark).getStartAccessors(), + ) + assertEquals(setOf(FinalAccessor), m.finalOf().getStartAccessors()) + } + + @Test + fun `start accessors expose the structural head before a semantic mark`() { + val m = mgr(true) + assertEquals( + setOf(field), + m.finalOf(field, AnyAccessor, mark).getStartAccessors(), + ) + } + + @Test + fun `static kept before field on both fact sides`() { + val m = mgr(true) + val expected = m.finalOf(stat, field, AnyAccessor, mark) + val actual = m.finalOf(field, AnyAccessor, mark).prependAccessor(stat) + assertEquals(expected, actual) + } + + @Test + fun `value type wrapper is distinct and group read exposes the normal residual`() { + val m = mgr(true) + val typed = m.finalOf(TypeInfoGroupAccessor, typeInfo) + assertFalse(m.finalOf(typeInfo) == typed) + assertTrue(typed.startsWithAccessor(TypeInfoGroupAccessor)) + val residual = typed.readAccessor(TypeInfoGroupAccessor)!! + assertEquals(m.finalOf(typeInfo), residual) + assertEquals( + setOf(AnyAccessor, typeInfo), + residual.getStartAccessors(), + ) + assertEquals(typed, typed.clearAccessor(TypeInfoGroupAccessor)) + assertEquals(typed, typed.clearAccessor(typeInfo)) + } + + @Test + fun `type info fact enumerates as the collapsed group-type pair`() { + val m = mgr(true) + val typed = m.finalOf(TypeInfoGroupAccessor, typeInfo) + assertEquals(1, typed.size) + assertEquals( + setOf(TypeInfoGroupAccessor, typeInfo, FinalAccessor), + typed.getAllAccessors(), + ) + } + + @Test + fun `value accessor states have exact read and clear behavior`() { + val m = mgr(true) + val directMark = m.finalOf(mark) + val valueMark = m.finalOf(ValueAccessor, mark) + assertEquals(setOf(AnyAccessor, mark), directMark.getStartAccessors()) + assertEquals(setOf(AnyAccessor, ValueAccessor), valueMark.getStartAccessors()) + assertEquals(directMark, valueMark.readAccessor(ValueAccessor)) + assertEquals(directMark, directMark.clearAccessor(mark)) + assertEquals(valueMark, valueMark.clearAccessor(ValueAccessor)) + + val directType = m.finalOf(typeInfo) + val groupedType = m.finalOf(TypeInfoGroupAccessor, typeInfo) + assertEquals(setOf(AnyAccessor, typeInfo), directType.getStartAccessors()) + assertEquals(setOf(AnyAccessor, TypeInfoGroupAccessor), groupedType.getStartAccessors()) + assertEquals(directType, groupedType.readAccessor(TypeInfoGroupAccessor)) + assertEquals(directType, directType.clearAccessor(typeInfo)) + assertEquals(groupedType, groupedType.clearAccessor(TypeInfoGroupAccessor)) + } + + @Test + fun `type info group is absent without a type accessor`() { + val m = mgr(false) + val plain = m.finalOf(mark) + assertFalse(plain.startsWithAccessor(TypeInfoGroupAccessor)) + assertNull(plain.readAccessor(TypeInfoGroupAccessor)) + } + + @Test + fun `initial fact ops mirror final`() { + val m = mgr(true) + var i = m.createFinalInitialAp(arg0, ExclusionSet.Empty) as BaseOnlyInitialFactAp + i = i.prependAccessor(mark) as BaseOnlyInitialFactAp + i = i.prependAccessor(AnyAccessor) as BaseOnlyInitialFactAp + assertTrue(i.startsWithAccessor(field)) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactSetTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactSetTest.kt new file mode 100644 index 000000000..3b54cbf47 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactSetTest.kt @@ -0,0 +1,697 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.Edge +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactToFactEdgeBuilder +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.LanguageManager +import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.serialization.MethodContextSerializer +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonCallExpr +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class BaseOnlyFactSetTest { + private val mark = TaintMarkAccessor("m") + private val field1 = FieldAccessor("A", "f", "B") + private val field2 = FieldAccessor("A", "g", "B") + + private fun mkManager( + fieldSensitive: Boolean = false, + fieldGeneralizationEnabled: Boolean = true, + summaryStorageFieldGeneralizationEnabled: Boolean = false, + ) = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + org.opentaint.dataflow.util.Cancellation(), + fieldSensitive = fieldSensitive, + fieldGeneralizationEnabled = fieldGeneralizationEnabled, + summaryStorageFieldGeneralizationEnabled = summaryStorageFieldGeneralizationEnabled, + ) + + private val dummyMethod = object : CommonMethod { + override val name: String = "dummy" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + + private val inst = object : CommonInst { + override fun toString(): String = "i0" + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod = dummyMethod + } + } + + private val lm = object : LanguageManager { + override fun getInstIndex(inst: CommonInst): Int = 0 + override fun getMaxInstIndex(method: CommonMethod): Int = 0 + override fun getInstByIndex(method: CommonMethod, index: Int): CommonInst = error("unused") + override fun isEmpty(method: CommonMethod): Boolean = error("unused") + override fun getCallExpr(inst: CommonInst): CommonCallExpr? = null + override fun producesExceptionalControlFlow(inst: CommonInst): Boolean = false + override fun getCalleeMethod(callExpr: CommonCallExpr): CommonMethod = error("unused") + override val methodContextSerializer: MethodContextSerializer get() = error("unused") + } + + private fun BaseOnlyApManager.finalFact(base: AccessPathBase, vararg accessors: Accessor): FinalFactAp { + var fact = createFinalAp(base, ExclusionSet.Universe) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + @Test + fun `z2f canonicalizes explicit Any to the implicit structural branch`() { + val m = mkManager() + val set = m.methodEdgesFinalApSet(inst, 0, lm) + + val anyMark = m.finalFact(AccessPathBase.This, AnyAccessor, mark) + val added1 = set.add(inst, anyMark) + assertNotNull(added1, "first add returns a fact") + assertTrue(added1.startsWithAccessor(field1), "returned fact is any-expanded (field insensitive)") + + val bareMark = m.finalFact(AccessPathBase.This, mark) + assertNull(set.add(inst, bareMark), "explicit Any and implicit Any have one storage key") + + val collected = mutableListOf() + set.collectApAtStatement(collected, inst) + assertEquals(1, collected.size) + } + + @Test + fun `z2f preserves the implicit structural branch of a bare mark`() { + val m = mkManager() + val set = m.methodEdgesFinalApSet(inst, 0, lm) + val added = set.add(inst, m.finalFact(AccessPathBase.This, mark)) + assertNotNull(added) + assertTrue(added.startsWithAccessor(field1)) + } + + @Test + fun `z2f keeps distinct fields when field extension enabled`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesFinalApSet(inst, 0, lm) + assertNotNull(set.add(inst, m.finalFact(AccessPathBase.This, field1, mark))) + assertNotNull(set.add(inst, m.finalFact(AccessPathBase.This, field2, mark)), "distinct field kept under extension") + } + + @Test + fun `f2f dedups and returns on new edge`() { + val m = mkManager() + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val initial = m.mostAbstractInitialAp(AccessPathBase.Return).replaceExclusions(ExclusionSet.Empty) + val final = m.createFinalAp(AccessPathBase.This, ExclusionSet.Empty).prependAccessor(mark) + + assertEquals(1, set.add(inst, initial, final).size, "first f2f edge is new") + assertTrue(set.add(inst, initial, final).isEmpty(), "same f2f edge subsumed") + + val collected = mutableListOf>() + set.collectApAtStatement(collected, inst) + assertEquals(1, collected.size) + } + + @Test + fun `f2f keeps a final coverage antichain`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val initial = m.mostAbstractInitialAp(AccessPathBase.Return).replaceExclusions(ExclusionSet.Empty) + val fieldFinal = m.finalFact(AccessPathBase.This, field1, mark) + .replaceExclusions(ExclusionSet.Empty) + val generalFinal = m.finalFact(AccessPathBase.This, mark) + .replaceExclusions(ExclusionSet.Empty) + + assertEquals(listOf(initial to fieldFinal), set.add(inst, initial, fieldFinal)) + assertEquals(listOf(initial to generalFinal), set.add(inst, initial, generalFinal)) + assertTrue( + set.add(inst, initial, fieldFinal).isEmpty(), + "a final already covered by the stored abstract final is not republished", + ) + + val collected = mutableListOf>() + set.collectApAtStatement(collected, inst) + assertEquals(listOf(initial to generalFinal), collected) + } + + @Test + fun `covered final still contributes to shared exclusion state`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val ex1 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-general")) + val ex2 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-covered")) + val initial1 = m.mostAbstractInitialAp(AccessPathBase.Return).replaceExclusions(ex1) + val initial2 = initial1.replaceExclusions(ex2) + val generalFinal = m.finalFact(AccessPathBase.This, mark).replaceExclusions(ex1) + val coveredFinal = m.finalFact(AccessPathBase.This, field1, mark).replaceExclusions(ex2) + + assertEquals(listOf(initial1 to generalFinal), set.add(inst, initial1, generalFinal)) + val delta = set.add(inst, initial2, coveredFinal) + + assertEquals(1, delta.size) + assertEquals(ex1.union(ex2), delta.single().first.exclusions) + assertEquals(ex1.union(ex2), delta.single().second.exclusions) + assertTrue( + BaseOnlyAccessOps.covers( + (delta.single().second as BaseOnlyFinalFactAp).access, + (coveredFinal as BaseOnlyFinalFactAp).access, + ) + ) + } + + @Test + fun `f2f shares Tree fact-state exclusion union across its final language`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val ex1 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-1")) + val ex2 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-2")) + val initial1 = m.mostAbstractInitialAp(AccessPathBase.Return).replaceExclusions(ex1) + val initial2 = initial1.replaceExclusions(ex2) + val final1 = m.finalFact(AccessPathBase.This, field1, mark).replaceExclusions(ex1) + val final2 = m.finalFact(AccessPathBase.This, field2, mark).replaceExclusions(ex2) + + assertEquals(1, set.add(inst, initial1, final1).size) + val delta = set.add(inst, initial2, final2) + assertEquals(2, delta.size, "an exclusion change re-emits the complete final language") + assertTrue(delta.all { it.first.exclusions == ex1.union(ex2) }) + assertTrue(delta.all { it.second.exclusions == ex1.union(ex2) }) + + val collected = mutableListOf>() + set.collectApAtStatement(collected, inst) + assertEquals(2, collected.size) + assertTrue(collected.all { it.first.exclusions == ex1.union(ex2) }) + assertTrue(collected.all { it.second.exclusions == ex1.union(ex2) }) + } + + @Test + fun `f2f exclusion update retains Normal and Value finals separately`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val ex1 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-direct")) + val ex2 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-wrapped")) + val initial1 = m.mostAbstractInitialAp(AccessPathBase.Return).replaceExclusions(ex1) + val initial2 = initial1.replaceExclusions(ex2) + val normal = m.finalFact(AccessPathBase.This, mark).replaceExclusions(ex1) as BaseOnlyFinalFactAp + val value = m.finalFact(AccessPathBase.This, ValueAccessor, mark) + .replaceExclusions(ex2) as BaseOnlyFinalFactAp + + assertEquals(BaseOnlyValueAccessorState.Normal, normal.access.valueAccessorState) + assertEquals(BaseOnlyValueAccessorState.Value, value.access.valueAccessorState) + assertEquals(1, set.add(inst, initial1, normal).size) + val delta = set.add(inst, initial2, value) + assertEquals(2, delta.size, "Normal and Value finals must both be re-emitted") + assertEquals( + setOf(BaseOnlyValueAccessorState.Normal, BaseOnlyValueAccessorState.Value), + delta.map { (it.second as BaseOnlyFinalFactAp).access.valueAccessorState }.toSet(), + ) + assertTrue(delta.all { it.second.exclusions == ex1.union(ex2) }) + + val collected = mutableListOf>() + set.collectApAtStatement(collected, inst) + assertEquals(2, collected.size) + assertEquals( + setOf(BaseOnlyValueAccessorState.Normal, BaseOnlyValueAccessorState.Value), + collected.map { (it.second as BaseOnlyFinalFactAp).access.valueAccessorState }.toSet(), + ) + assertTrue(collected.all { it.second.exclusions == ex1.union(ex2) }) + } + + @Test + fun `method edges publish every BaseOnly final changed by exclusion aggregation`() { + val m = mkManager(fieldSensitive = true) + val methodEntryPoint = MethodEntryPoint(EmptyMethodContext, inst) + val edges = MethodAnalyzerEdges(m, methodEntryPoint, lm) + val ex1 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-direct")) + val ex2 = ExclusionSet.Concrete(TaintMarkAccessor("excluded-wrapped")) + val initial1 = m.mostAbstractInitialAp(AccessPathBase.Return).replaceExclusions(ex1) + val initial2 = initial1.replaceExclusions(ex2) + val normal = m.finalFact(AccessPathBase.This, mark).replaceExclusions(ex1) + val value = m.finalFact(AccessPathBase.This, ValueAccessor, mark).replaceExclusions(ex2) + + assertEquals(1, edges.add(Edge.FactToFact(methodEntryPoint, initial1, inst, normal)).size) + val delta = edges.add(Edge.FactToFact(methodEntryPoint, initial2, inst, value)) + + assertEquals(2, delta.size) + val factEdges = delta.map { it as Edge.FactToFact } + assertTrue(factEdges.all { it.initialFactAp.exclusions == ex1.union(ex2) }) + assertTrue(factEdges.all { it.factAp.exclusions == ex1.union(ex2) }) + assertEquals( + setOf(BaseOnlyValueAccessorState.Normal, BaseOnlyValueAccessorState.Value), + factEdges.map { (it.factAp as BaseOnlyFinalFactAp).access.valueAccessorState }.toSet(), + ) + } + + @Test + fun `f2f trace lookup resolves a suffix alias to its field abstract primary`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val primary = BaseOnlyInitialFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR), + ExclusionSet.Empty, + ) + val alias = BaseOnlyInitialFactAp( + m, + AccessPathBase.Return, + ABSTRACT_EMPTY_ACCESS, + ExclusionSet.Empty, + ) + val final = m.finalFact(AccessPathBase.This, field1, mark).replaceExclusions(ExclusionSet.Empty) + + assertEquals(1, set.add(inst, primary, final).size) + m.enableTraceResolutionMode() + + val collected = mutableListOf() + set.collectApAtStatement( + collected, + inst, + alias, + m.mostAbstractInitialAp(AccessPathBase.This), + ) + assertEquals(listOf(final), collected) + } + + @Test + fun `f2f publishes correlated edges with different exact initial accesses`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val initialField = m.interner.index(FieldAccessor("Input", "field", "Value")) + val finalField = m.interner.index(FieldAccessor("Output", "field", "Value")) + val terminal = m.interner.index(TaintMarkAccessor("correlated-terminal")) + val broadInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.This, + packBaseOnlyAccess(NO_ACCESSOR, initialField, ABSTRACT_MARK), + ExclusionSet.Empty, + ) + val broadFinal = BaseOnlyFinalFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess(NO_ACCESSOR, finalField, ABSTRACT_MARK), + ExclusionSet.Empty, + ) + val concreteInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.This, + packBaseOnlyAccess(NO_ACCESSOR, initialField, terminal), + ExclusionSet.Empty, + ) + val concreteFinal = BaseOnlyFinalFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess(NO_ACCESSOR, finalField, terminal), + ExclusionSet.Empty, + ) + + assertEquals(listOf(broadInitial to broadFinal), set.add(inst, broadInitial, broadFinal)) + assertEquals( + listOf(concreteInitial to concreteFinal), + set.add(inst, concreteInitial, concreteFinal), + "summary-edge subsumption must not suppress an intraprocedural exact-initial edge", + ) + + val all = mutableListOf>() + set.collectApAtStatement(all, inst) + assertEquals(setOf(broadInitial to broadFinal, concreteInitial to concreteFinal), all.toSet()) + + val concreteLookup = mutableListOf() + set.collectApAtStatement( + concreteLookup, + inst, + concreteInitial, + m.mostAbstractInitialAp(AccessPathBase.Return), + ) + assertEquals(listOf(concreteFinal), concreteLookup) + } + + @Test + fun `f2f final pattern lookup remains exact after final index promotion`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val initial = BaseOnlyInitialFactAp( + m, + AccessPathBase.This, + ABSTRACT_EMPTY_ACCESS, + ExclusionSet.Empty, + ) + val terminal = m.interner.index(TaintMarkAccessor("indexed-terminal")) + val finals = (0 until 64).map { index -> + BaseOnlyFinalFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess( + NO_ACCESSOR, + m.interner.index(FieldAccessor("Indexed", "field$index", "Value")), + terminal, + ), + ExclusionSet.Empty, + ).also { set.add(inst, initial, it) } + } + val selected = finals[47] + val pattern = BaseOnlyInitialFactAp( + m, + selected.base, + selected.access, + ExclusionSet.Empty, + ) + + val collected = mutableListOf>() + set.collectApAtStatement(collected, inst, pattern) + + assertEquals(listOf>(initial to selected), collected) + } + + @Test + fun `f2f trace lookup erases an eligible exact witness without changing forward state`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val initialField = m.interner.index(FieldAccessor("Input", "field", "Value")) + val finalField = m.interner.index(FieldAccessor("Output", "field", "Value")) + val otherInitialField = m.interner.index(FieldAccessor("Input", "other", "Value")) + val otherFinalField = m.interner.index(FieldAccessor("Output", "other", "Value")) + val exA = ExclusionSet.Concrete(TaintMarkAccessor("trace-view-a")) + val exB = ExclusionSet.Concrete(TaintMarkAccessor("trace-view-b")) + val preciseInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.This, + packBaseOnlyAccess(NO_ACCESSOR, initialField, ABSTRACT_MARK), + exA, + ) + val preciseFinal = BaseOnlyFinalFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess(NO_ACCESSOR, finalField, ABSTRACT_MARK), + exA, + ) + val otherPreciseInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.This, + packBaseOnlyAccess(NO_ACCESSOR, otherInitialField, ABSTRACT_MARK), + exB, + ) + val otherPreciseFinal = BaseOnlyFinalFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess(NO_ACCESSOR, otherFinalField, ABSTRACT_MARK), + exB, + ) + val generalizedInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.This, + ABSTRACT_EMPTY_ACCESS, + exA.union(exB), + ) + val generalizedFinal = BaseOnlyFinalFactAp( + m, + AccessPathBase.Return, + ABSTRACT_EMPTY_ACCESS, + exA.union(exB), + ) + + assertEquals(listOf(preciseInitial to preciseFinal), set.add(inst, preciseInitial, preciseFinal)) + assertEquals( + listOf(otherPreciseInitial to otherPreciseFinal), + set.add(inst, otherPreciseInitial, otherPreciseFinal), + ) + + val forwardState = mutableListOf>() + set.collectApAtStatement(forwardState, inst) + assertEquals( + setOf>( + preciseInitial to preciseFinal, + otherPreciseInitial to otherPreciseFinal, + ), + forwardState.toSet(), + "the generalized witness is a trace-only view, not a primary forward edge", + ) + + m.enableTraceResolutionMode() + val traceLookup = mutableListOf() + set.collectApAtStatement( + traceLookup, + inst, + generalizedInitial, + m.mostAbstractInitialAp(AccessPathBase.Return), + ) + assertEquals( + listOf(generalizedFinal), + traceLookup, + "trace mode exposes a generalized witness without inserting it into the fact set", + ) + } + + @Test + fun `f2f field generalization is a trace only view`() { + val m = mkManager(fieldSensitive = true) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val exA = ExclusionSet.Concrete(TaintMarkAccessor("generalized-a")) + val exB = ExclusionSet.Concrete(TaintMarkAccessor("generalized-b")) + val generalizedExclusion = exA.union(exB) + val contributors = (0 until MAX_FIELD_ENUMERATION_EDGES + 2).map { index -> + val field = m.interner.index(FieldAccessor("Input", "field-$index", "Value")) + val exclusion = if (index % 2 == 0) exA else exB + val initial = BaseOnlyInitialFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess(NO_ACCESSOR, field, ABSTRACT_MARK), + exclusion, + ) + val final = BaseOnlyFinalFactAp( + m, + AccessPathBase.This, + ABSTRACT_EMPTY_ACCESS, + exclusion, + ) + initial to final + } + + contributors.forEach { (initial, final) -> + assertEquals( + listOf(initial to final), + set.add(inst, initial, final), + "forward insertion must retain every exact fact-set edge", + ) + } + + val generalizedInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.Return, + ABSTRACT_EMPTY_ACCESS, + generalizedExclusion, + ) + val generalizedFinal = BaseOnlyFinalFactAp( + m, + AccessPathBase.This, + ABSTRACT_EMPTY_ACCESS, + generalizedExclusion, + ) + + val forward = mutableListOf>() + set.collectApAtStatement(forward, inst) + assertEquals( + contributors.toSet(), + forward.toSet(), + "forward collection must remain exact", + ) + + m.enableTraceResolutionMode() + val traceState = mutableListOf>() + set.collectApAtStatement(traceState, inst) + assertEquals( + contributors.toSet() + (generalizedInitial to generalizedFinal), + traceState.toSet(), + "trace mode adds one generalized view without replacing exact edges", + ) + + val traceLookup = mutableListOf() + set.collectApAtStatement( + traceLookup, + inst, + generalizedInitial, + m.mostAbstractInitialAp(AccessPathBase.This), + ) + assertEquals(listOf(generalizedFinal), traceLookup) + } + + @Test + fun `f2f trace view respects disabled field generalization`() { + val m = mkManager(fieldSensitive = true, fieldGeneralizationEnabled = false) + val set = m.methodEdgesInitialToFinalApSet(inst, 0, lm) + val exactInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.Return, + packBaseOnlyAccess(NO_ACCESSOR, m.interner.index(field1), ABSTRACT_MARK), + ExclusionSet.Empty, + ) + val exactFinal = BaseOnlyFinalFactAp( + m, + AccessPathBase.This, + ABSTRACT_EMPTY_ACCESS, + ExclusionSet.Empty, + ) + set.add(inst, exactInitial, exactFinal) + + m.enableTraceResolutionMode() + + val traceState = mutableListOf>() + set.collectApAtStatement(traceState, inst) + assertEquals( + listOf>(exactInitial to exactFinal), + traceState, + ) + + val generalizedInitial = BaseOnlyInitialFactAp( + m, + AccessPathBase.Return, + ABSTRACT_EMPTY_ACCESS, + ExclusionSet.Empty, + ) + val generalizedLookup = mutableListOf() + set.collectApAtStatement( + generalizedLookup, + inst, + generalizedInitial, + m.mostAbstractInitialAp(AccessPathBase.This), + ) + assertTrue(generalizedLookup.isEmpty()) + } + + @Test + fun `summary and fact trace generalization flags are independent`() { + fun collectedSizes( + factTraceGeneralization: Boolean, + summaryGeneralization: Boolean, + ): Pair { + val manager = mkManager( + fieldSensitive = true, + fieldGeneralizationEnabled = factTraceGeneralization, + summaryStorageFieldGeneralizationEnabled = summaryGeneralization, + ) + val factSet = manager.methodEdgesInitialToFinalApSet(inst, 0, lm) + val summaries = manager.methodInitialToFinalApSummariesStorage(inst) + val entryPoint = MethodEntryPoint(EmptyMethodContext, inst) + val edges = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + val initial = BaseOnlyInitialFactAp( + manager, + AccessPathBase.Return, + packBaseOnlyAccess( + NO_ACCESSOR, + manager.interner.index(FieldAccessor("Input", "isolated-$index", "Value")), + ABSTRACT_MARK, + ), + ExclusionSet.Empty, + ) + val final = BaseOnlyFinalFactAp( + manager, + AccessPathBase.This, + ABSTRACT_EMPTY_ACCESS, + ExclusionSet.Empty, + ) + factSet.add(inst, initial, final) + Edge.FactToFact(entryPoint, initial, inst, final) + } + + summaries.add(edges, mutableListOf()) + manager.enableTraceResolutionMode() + + val factViews = mutableListOf>() + factSet.collectApAtStatement(factViews, inst) + val summaryViews = mutableListOf() + summaries.filterEdgesTo( + summaryViews, + initialFactPattern = null, + finalFactBase = AccessPathBase.This, + ) + return factViews.size to summaryViews.size + } + + assertEquals( + (MAX_FIELD_ENUMERATION_EDGES + 1) to 1, + collectedSizes(factTraceGeneralization = false, summaryGeneralization = true), + "summary generalization must not add a projected fact-set view", + ) + assertEquals( + (MAX_FIELD_ENUMERATION_EDGES + 2) to (MAX_FIELD_ENUMERATION_EDGES + 1), + collectedSizes(factTraceGeneralization = true, summaryGeneralization = false), + "fact trace generalization must not generalize summary storage", + ) + } + + @Test + fun `nd f2f dedups`() { + val m = mkManager() + val set = m.methodEdgesNDInitialToFinalApSet(inst, 0, lm) + val i1 = m.mostAbstractInitialAp(AccessPathBase.This).prependAccessor(mark) + val i2 = m.mostAbstractInitialAp(AccessPathBase.Return).prependAccessor(mark) + val initial = setOf(i1, i2) + val final = m.finalFact(AccessPathBase.ClassStatic, mark) + + assertNotNull(set.add(inst, initial, final)) + assertNull(set.add(inst, initial, final)) + } + + @Test + fun `nd f2f canonicalizes initial exclusions before key publication`() { + val m = mkManager() + val set = m.methodEdgesNDInitialToFinalApSet(inst, 0, lm) + val concrete = ExclusionSet.Concrete(TaintMarkAccessor("excluded")) + val supplied = setOf(m.mostAbstractInitialAp(AccessPathBase.This).replaceExclusions(concrete)) + val canonical = supplied.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) } + val final = m.finalFact(AccessPathBase.ClassStatic, mark) + + val added = assertNotNull(set.add(inst, supplied, final)) + assertEquals(canonical, added.first) + assertNull(set.add(inst, canonical, final), "equivalent canonical key is idempotent") + + val found = mutableListOf() + set.collectApAtStatement(found, inst, canonical, m.mostAbstractInitialAp(AccessPathBase.ClassStatic)) + assertEquals(1, found.size) + } + + @Test + fun `final fact list rejects transient collapsed access without shifting its arrays`() { + val m = mkManager() + val list = m.finalFactList() + val collapsed = BaseOnlyFinalFactAp( + m, + AccessPathBase.This, + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, COLLAPSED_MARK), + ExclusionSet.Empty, + ) + list.add(collapsed) + + val valid = m.finalFact(AccessPathBase.Return, mark) + list.add(valid) + assertEquals(valid, list.get(0)) + assertFailsWith { list.get(1) } + assertEquals(valid, list.removeLast()) + assertFailsWith { list.removeLast() } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndexTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndexTest.kt new file mode 100644 index 000000000..297b2166d --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndexTest.kt @@ -0,0 +1,252 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.SideEffectKind +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.util.Cancellation +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class BaseOnlyInitialAccessIndexTest { + @Test + fun `pattern traversal agrees with summary applicability for every packed slot shape`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val staticA = manager.interner.index(ClassStaticAccessor("S0")) + val staticB = manager.interner.index(ClassStaticAccessor("S1")) + val fieldA = manager.interner.index(FieldAccessor("C", "f0", "T")) + val fieldB = manager.interner.index(FieldAccessor("C", "f1", "T")) + val markA = manager.interner.index(TaintMarkAccessor("m0")) + val markB = manager.interner.index(TaintMarkAccessor("m1")) + val accesses = buildList { + for (staticIdx in intArrayOf(ABSTRACT_MARK, NO_ACCESSOR, staticA, staticB)) { + for (fieldIdx in intArrayOf(ABSTRACT_MARK, NO_ACCESSOR, fieldA, fieldB)) { + for (suffixIdx in intArrayOf(ABSTRACT_MARK, NO_ACCESSOR, markA, markB)) { + val modes = if (suffixIdx == markA || suffixIdx == markB) { + BaseOnlyValueAccessorState.entries + } else { + listOf(BaseOnlyValueAccessorState.Normal) + } + for (mode in modes) { + if (staticIdx == ABSTRACT_MARK && + (fieldIdx != NO_ACCESSOR || suffixIdx != NO_ACCESSOR) + ) continue + if (fieldIdx == ABSTRACT_MARK && suffixIdx != NO_ACCESSOR) continue + if (suffixIdx == NO_ACCESSOR && (staticIdx >= 0 || fieldIdx >= 0)) continue + if (staticIdx == NO_ACCESSOR && fieldIdx == NO_ACCESSOR && suffixIdx == NO_ACCESSOR) continue + val access = packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, mode) + add(access) + } + } + } + } + } + val index = BaseOnlyInitialAccessIndex() + accesses.forEach { access -> index.getOrCreate(access) { access } } + accesses.forEach { access -> assertEquals(access, index.get(access)) } + + for (pattern in accesses) { + val actual = hashSetOf() + index.collectCandidates(pattern) { access, value -> + assertEquals(access, value) + if (baseOnlySummaryInitialMatches(pattern, access)) actual += access + } + val expected = accesses.filterTo(hashSetOf()) { baseOnlySummaryInitialMatches(pattern, it) } + assertEquals(expected, actual, "pattern=$pattern") + } + + val all = hashSetOf() + index.collectAll { access, _ -> all += access } + assertEquals(accesses.toSet(), all) + } + + @Test + fun `f2f identity and non-identity summaries use the same pattern filter`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(testInst, manager).createStorage() + val fieldA = manager.interner.index(FieldAccessor("C", "first", "T")) + val fieldB = manager.interner.index(FieldAccessor("C", "second", "T")) + val fieldC = manager.interner.index(FieldAccessor("C", "identity", "T")) + val mark = manager.interner.index(TaintMarkAccessor("initial")) + val finalA = manager.interner.index(TaintMarkAccessor("final-a")) + val finalB = manager.interner.index(TaintMarkAccessor("final-b")) + val first = packBaseOnlyAccess(NO_ACCESSOR, fieldA, mark) + val second = packBaseOnlyAccess(NO_ACCESSOR, fieldB, mark) + val identity = packBaseOnlyAccess(NO_ACCESSOR, fieldC, mark) + storage.add( + listOf( + edge(first, packBaseOnlyAccess(NO_ACCESSOR, fieldA, finalA)), + edge(second, packBaseOnlyAccess(NO_ACCESSOR, fieldB, finalB)), + edge(identity, identity), + ), + mutableListOf(), + ) + + assertEquals(1, storage.query(first)) + assertEquals(1, storage.query(second)) + assertEquals(1, storage.query(identity), "identity summaries must be filtered too") + assertEquals(3, storage.query(packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR))) + assertEquals(3, storage.query(null)) + } + + @Test + fun `identity trie traversal agrees with summary applicability`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(testInst, manager).createStorage() + val static = manager.interner.index(ClassStaticAccessor("S")) + val fieldA = manager.interner.index(FieldAccessor("C", "f0", "T")) + val fieldB = manager.interner.index(FieldAccessor("C", "f1", "T")) + val markA = manager.interner.index(TaintMarkAccessor("m0")) + val markB = manager.interner.index(TaintMarkAccessor("m1")) + val initials = buildList { + for (staticIdx in intArrayOf(NO_ACCESSOR, static)) { + for (fieldIdx in intArrayOf(NO_ACCESSOR, fieldA, fieldB)) { + for (suffixIdx in intArrayOf(NO_ACCESSOR, markA, markB)) { + val access = packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx) + if (!access.isEmpty && suffixIdx != NO_ACCESSOR) add(access) + } + } + } + } + storage.add(initials.map { edge(it, it) }, mutableListOf()) + + val patterns = initials + listOf( + packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR), + packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR), + packBaseOnlyAccess(static, ABSTRACT_MARK, NO_ACCESSOR), + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK), + packBaseOnlyAccess(NO_ACCESSOR, fieldA, ABSTRACT_MARK), + ) + patterns.forEach { pattern -> + val expected = initials.count { baseOnlySummaryInitialMatches(pattern, it) } + assertEquals(expected, storage.query(pattern), "pattern=$pattern") + } + } + + @Test + fun `fact side-effect summaries filter incompatible initials`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val storage = FactSESummariesBaseOnlyStorage(testInst, manager).createStorage() + val kind = object : SideEffectKind {} + val fieldA = manager.interner.index(FieldAccessor("C", "f0", "T")) + val fieldB = manager.interner.index(FieldAccessor("C", "f1", "T")) + val mark = manager.interner.index(TaintMarkAccessor("effect")) + val first = packBaseOnlyAccess(NO_ACCESSOR, fieldA, mark) + val second = packBaseOnlyAccess(NO_ACCESSOR, fieldB, mark) + storage.add(first, mapOf(kind to ExclusionSet.Empty), mutableListOf()) + storage.add(second, mapOf(kind to ExclusionSet.Empty), mutableListOf()) + + assertEquals(1, storage.query(first)) + assertEquals(1, storage.query(second)) + assertEquals(2, storage.query(packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR))) + assertEquals(2, storage.query(null)) + } + + @Test + fun `single writer and concurrent readers survive repeated index rehashes`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val index = BaseOnlyInitialAccessIndex() + val accesses = (0 until 4_000).map { value -> + val static = manager.interner.index(ClassStaticAccessor("S${value / 1_000}")) + val field = manager.interner.index(FieldAccessor("C", "f$value", "T")) + val mark = manager.interner.index(TaintMarkAccessor("m$value")) + packBaseOnlyAccess(static, field, mark) + } + val readerStatics = IntArray(4) { reader -> manager.interner.index(ClassStaticAccessor("S$reader")) } + val failures = ConcurrentLinkedQueue() + val executor = Executors.newFixedThreadPool(5) + + executor.submit { + try { + accesses.forEach { access -> index.getOrCreate(access) { access } } + } catch (t: Throwable) { + failures += t + } + } + repeat(4) { reader -> + executor.submit { + try { + repeat(250) { + val pattern = if (reader % 2 == 0) { + packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR) + } else { + packBaseOnlyAccess(readerStatics[reader], ABSTRACT_MARK, NO_ACCESSOR) + } + index.collectCandidates(pattern) { access, value -> + assertEquals(access, value) + // Routing may conservatively return false-positive candidates. + } + } + } catch (t: Throwable) { + failures += t + } + } + } + + executor.shutdown() + assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS)) + assertTrue(failures.isEmpty(), failures.joinToString("\n")) + + val eventual = hashSetOf() + index.collectAll { access, value -> + assertEquals(access, value) + eventual += access + } + assertEquals(accesses.toSet(), eventual) + } + + private fun edge(initial: BaseOnlyAccess, final: BaseOnlyAccess) = + org.opentaint.dataflow.ap.ifds.access.common.CommonF2FSummary.StorageEdge( + initial, + final, + ExclusionSet.Empty, + ) + + private fun org.opentaint.dataflow.ap.ifds.access.common.CommonF2FSummary.Storage.query( + pattern: BaseOnlyAccess?, + ): Int { + val result = mutableListOf>() + collectSummariesTo(result, pattern) + return result.size + } + + private fun org.opentaint.dataflow.ap.ifds.access.common.CommonFactSideEffectSummary.Storage.query( + pattern: BaseOnlyAccess?, + ): Int { + val result = mutableListOf>() + collectSummariesTo(result, pattern) + return result.size + } +} + +private val testInst = object : org.opentaint.ir.api.common.cfg.CommonInst { + override val location: org.opentaint.ir.api.common.cfg.CommonInstLocation = + object : org.opentaint.ir.api.common.cfg.CommonInstLocation { + override val method: org.opentaint.ir.api.common.CommonMethod + get() = testMethod + } +} + +private val testMethod = object : org.opentaint.ir.api.common.CommonMethod { + override val name: String = "baseOnlyInitialAccessIndex" + override val parameters: List = emptyList() + override val returnType: org.opentaint.ir.api.common.CommonTypeName = + object : org.opentaint.ir.api.common.CommonTypeName { + override val typeName: String = "java.lang.Object" + } + + override fun flowGraph(): org.opentaint.ir.api.common.cfg.ControlFlowGraph = + object : org.opentaint.ir.api.common.cfg.ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: org.opentaint.ir.api.common.cfg.CommonInst) = emptySet() + override fun predecessors(node: org.opentaint.ir.api.common.cfg.CommonInst) = emptySet() + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionCasesTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionCasesTest.kt new file mode 100644 index 000000000..ca9daeb8f --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionCasesTest.kt @@ -0,0 +1,389 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlyInitialFactAbstractionCasesTest { + private val arg0 = AccessPathBase.Argument(0) + private val field = FieldAccessor("A", "f", "B") + private val mark = TaintMarkAccessor("m") + + private fun mgr(fieldSensitive: Boolean = false) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.finalOf(vararg accessors: Accessor): FinalFactAp { + var f = createFinalAp(arg0, ExclusionSet.Empty) + accessors.reversed().forEach { f = f.prependAccessor(it) } + return f + } + + private fun BaseOnlyApManager.analyzedExcluding(vararg excluded: Accessor): InitialFactAp { + var f = mostAbstractInitialAp(arg0) + excluded.forEach { f = f.exclude(it) } + return f + } + + private fun BaseOnlyApManager.acc(vararg accessors: Accessor, abstract: Boolean): BaseOnlyAccess = + BaseOnlyAccessOps.build(IntArray(accessors.size) { interner.index(accessors[it]) }, abstract) + + private fun contains( + produced: List>, + initialAccess: BaseOnlyAccess, + finalAccess: BaseOnlyAccess, + ): Boolean = produced.any { (initial, final) -> + initial as BaseOnlyInitialFactAp + final as BaseOnlyFinalFactAp + initial.base == arg0 && initial.access == initialAccess && final.access == finalAccess + } + + @Test + fun `case A treats explicit any as the implicit structural projection`() { + val m = mgr(false) + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.registerNewInitialFact(m.analyzedExcluding(mark), FactTypeChecker.Dummy) + + val produced = abstraction.addAbstractedInitialFact(m.finalOf(AnyAccessor, mark), FactTypeChecker.Dummy) + + assertTrue(contains(produced, m.acc(abstract = true), m.acc(abstract = true))) + assertTrue( + contains( + produced, + m.acc(mark, FinalAccessor, abstract = false), + m.acc(mark, abstract = false), + ) + ) + } + + @Test + fun `case A emits only any-star when mark not excluded`() { + val m = mgr(false) + val abstraction = BaseOnlyInitialFactAbstraction(m) + + val produced = abstraction.addAbstractedInitialFact(m.finalOf(AnyAccessor, mark), FactTypeChecker.Dummy) + + assertTrue(contains(produced, m.acc(abstract = true), m.acc(abstract = true))) + assertFalse( + contains( + produced, + m.acc(mark, FinalAccessor, abstract = false), + m.acc(mark, abstract = false), + ) + ) + } + + @Test + fun `case B emits base-star then field-any layers gated by exclusions`() { + val m = mgr(true) + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.registerNewInitialFact(m.analyzedExcluding(field, mark), FactTypeChecker.Dummy) + + val produced = abstraction.addAbstractedInitialFact(m.finalOf(field, AnyAccessor, mark), FactTypeChecker.Dummy) + + val fieldAp = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) + assertTrue(contains(produced, fieldAp, fieldAp)) + assertTrue( + contains(produced, m.acc(field, abstract = true), m.acc(field, abstract = true)) + ) + assertTrue( + contains( + produced, + m.acc(field, mark, FinalAccessor, abstract = false), + m.acc(field, mark, abstract = false), + ) + ) + } + + @Test + fun `case B stops at base-star when field not excluded`() { + val m = mgr(true) + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.registerNewInitialFact(m.analyzedExcluding(mark), FactTypeChecker.Dummy) + + val produced = abstraction.addAbstractedInitialFact(m.finalOf(field, AnyAccessor, mark), FactTypeChecker.Dummy) + + val fieldAp = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) + assertTrue(contains(produced, fieldAp, fieldAp)) + assertFalse( + contains(produced, m.acc(field, abstract = true), m.acc(field, abstract = true)) + ) + } + + @Test + fun `same added fact twice abstracts only once`() { + val m = mgr(false) + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.registerNewInitialFact(m.analyzedExcluding(mark), FactTypeChecker.Dummy) + + val added = m.finalOf(AnyAccessor, mark) + val first = abstraction.addAbstractedInitialFact(added, FactTypeChecker.Dummy) + val second = abstraction.addAbstractedInitialFact(added, FactTypeChecker.Dummy) + + assertTrue(first.isNotEmpty()) + assertTrue(second.isEmpty()) + } + + @Test + fun `mark-less value on a static abstracts to a covering final never open`() { + val m = mgr(false) + val stat = ClassStaticAccessor("S") + val abstraction = BaseOnlyInitialFactAbstraction(m) + + val valueOnStatic = BaseOnlyFinalFactAp(m, arg0, m.acc(stat, FinalAccessor, abstract = false), ExclusionSet.Empty) + val produced = abstraction.addAbstractedInitialFact(valueOnStatic, FactTypeChecker.Dummy) + + val staticAp = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) + assertTrue(contains(produced, staticAp, staticAp)) + assertTrue(produced.none { (_, final) -> + (final as BaseOnlyFinalFactAp).access.let { !it.isEmpty && !it.hasAp && it.suffixIdx == NO_ACCESSOR } + }) + } + + @Test + fun `abstracting a field-abstract fact yields an open field-abstract initial not a closed value`() { + val m = mgr(fieldSensitive = true) + val abstraction = BaseOnlyInitialFactAbstraction(m) + + val fieldAbstract = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) + val fact = BaseOnlyFinalFactAp(m, arg0, fieldAbstract, ExclusionSet.Empty) + + val produced = abstraction.addAbstractedInitialFact(fact, FactTypeChecker.Dummy) + + assertTrue( + contains(produced, fieldAbstract, fieldAbstract), + "a field-abstract added fact must abstract to an open field-abstract initial, got: $produced", + ) + val closedValue = m.acc(FinalAccessor, abstract = false) + assertFalse( + produced.any { (initial, _) -> (initial as BaseOnlyInitialFactAp).access == closedValue }, + "a field-abstract added fact must not collapse to a closed value initial, got: $produced", + ) + } + + @Test + fun `ladder starts fully abstract then walks the abstraction point rightward`() { + val m = mgr(false) + val stat = ClassStaticAccessor("S") + val abstraction = BaseOnlyInitialFactAbstraction(m) + + val fact = BaseOnlyFinalFactAp(m, arg0, m.acc(stat, mark, abstract = false), ExclusionSet.Empty) + val staticAp = BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) + val markAp = BaseOnlyAccessOps.abstractAt(m.interner.index(stat), NO_ACCESSOR, 2) + + val first = abstraction.addAbstractedInitialFact(fact, FactTypeChecker.Dummy) + assertTrue(contains(first, staticAp, staticAp)) + assertFalse(contains(first, markAp, markAp)) + + val second = abstraction.registerNewInitialFact(m.analyzedExcluding(stat), FactTypeChecker.Dummy) + assertTrue(contains(second, markAp, markAp)) + } + + @Test + fun `excluding a later accessor waits until the current blocker is excluded`() { + val m = mgr(fieldSensitive = true) + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.addAbstractedInitialFact(m.finalOf(field, mark), FactTypeChecker.Dummy) + + val laterOnly = abstraction.registerNewInitialFact( + m.analyzedExcluding(mark), + FactTypeChecker.Dummy, + ) + assertTrue(laterOnly.isEmpty(), "the mark is unreachable while the field still blocks abstraction") + + val unblocked = abstraction.registerNewInitialFact( + m.analyzedExcluding(field), + FactTypeChecker.Dummy, + ) + assertTrue(contains(unblocked, m.acc(field, abstract = true), m.acc(field, abstract = true))) + assertTrue( + contains( + unblocked, + m.acc(field, mark, FinalAccessor, abstract = false), + m.acc(field, mark, abstract = false), + ), + "excluding the field must advance across the mark that was excluded earlier", + ) + } + + @Test + fun `exclusion below one field does not unblock a sibling field`() { + val m = mgr(fieldSensitive = true) + val sibling = FieldAccessor("A", "sibling", "B") + val abstraction = BaseOnlyInitialFactAbstraction(m) + + abstraction.addAbstractedInitialFact(m.finalOf(sibling, mark), FactTypeChecker.Dummy) + abstraction.registerNewInitialFact( + m.mostAbstractInitialAp(arg0).exclude(sibling), + FactTypeChecker.Dummy, + ) + + var fieldScopedDemand = m.mostAbstractInitialAp(arg0).prependAccessor(field) + fieldScopedDemand = fieldScopedDemand.exclude(mark) + val produced = abstraction.registerNewInitialFact(fieldScopedDemand, FactTypeChecker.Dummy) + + assertTrue( + produced.isEmpty(), + "an exclusion at $field.* must not unblock $sibling.$mark, got $produced", + ) + } + + @Test + fun `one exclusion update advances across every newly excluded blocker`() { + val m = mgr(fieldSensitive = true) + val stat = ClassStaticAccessor("S") + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.addAbstractedInitialFact(m.finalOf(stat, field, mark), FactTypeChecker.Dummy) + + val produced = abstraction.registerNewInitialFact( + m.analyzedExcluding(stat, field, mark), + FactTypeChecker.Dummy, + ) + + assertTrue( + contains( + produced, + m.acc(stat, field, mark, FinalAccessor, abstract = false), + m.acc(stat, field, mark, abstract = false), + ), + "all exclusions must be installed before an indexed fact advances", + ) + } + + @Test + fun `refinement on type group retains the separate direct-type fact and still abstracts`() { + val m = mgr(false) + val typeInfo = TypeInfoAccessor("pkg.fn") + val abstraction = BaseOnlyInitialFactAbstraction(m) + + val demand = m.analyzedExcluding(TypeInfoGroupAccessor) + abstraction.registerNewInitialFact(demand, FactTypeChecker.Dummy) + + val wrapped = m.finalOf(TypeInfoGroupAccessor, typeInfo) as BaseOnlyFinalFactAp + val direct = m.finalOf(typeInfo) as BaseOnlyFinalFactAp + assertTrue(wrapped.access != direct.access) + + assertTrue( + direct.delta(demand).any { it is BaseOnlyNodeFinalDelta }, + "the separate direct-type fact survives exclusion of the group branch", + ) + + val produced = abstraction.addAbstractedInitialFact(direct, FactTypeChecker.Dummy) + assertTrue(contains(produced, m.acc(abstract = true), m.acc(abstract = true))) + } + + @Test + fun `refinement on type group after the fact emits the refined type fact`() { + val m = mgr(false) + val typeInfo = TypeInfoAccessor("pkg.fn") + val abstraction = BaseOnlyInitialFactAbstraction(m) + + abstraction.addAbstractedInitialFact(m.finalOf(TypeInfoGroupAccessor, typeInfo), FactTypeChecker.Dummy) + + val produced = abstraction.registerNewInitialFact( + m.analyzedExcluding(TypeInfoGroupAccessor), FactTypeChecker.Dummy, + ) + + val typeAp = m.acc(TypeInfoGroupAccessor, typeInfo, FinalAccessor, abstract = false) + assertTrue( + contains(produced, typeAp, typeAp), + "excluding the group must walk the wrapped branch and emit Group.Type.\$", + ) + } + + @Test + fun `refinement on the type accessor retains the compact group-type sibling`() { + val m = mgr(false) + val typeInfo = TypeInfoAccessor("pkg.fn") + + val demandExcludingType = m.analyzedExcluding(typeInfo) + val typed = m.finalOf(TypeInfoGroupAccessor, typeInfo) as BaseOnlyFinalFactAp + + assertTrue(typed.delta(demandExcludingType).any { it is BaseOnlyNodeFinalDelta }) + } + + @Test + fun `delta retains each value accessor state when the mark survives behind implicit Any`() { + val m = mgr(false) + val initialNoExclusion = m.mostAbstractInitialAp(arg0).prependAccessor(AnyAccessor) + val initialExcludingMark = initialNoExclusion.exclude(mark) + + for (final in listOf( + m.finalOf(AnyAccessor, mark) as BaseOnlyFinalFactAp, + m.finalOf(AnyAccessor, ValueAccessor, mark) as BaseOnlyFinalFactAp, + )) { + assertTrue(final.delta(initialNoExclusion).any { !it.isEmpty }) + val retained = final.delta(initialExcludingMark).single() as BaseOnlyNodeFinalDelta + assertEquals(final.access.valueAccessorState, retained.access.valueAccessorState) + } + } + + private fun assertNoMixedEdge(produced: List>) { + assertTrue( + produced.none { (initial, final) -> + (initial as BaseOnlyInitialFactAp); (final as BaseOnlyFinalFactAp) + !initial.access.hasAp && final.access.hasAp + }, + "no F2F edge may have a concrete initial and an abstract final, got $produced", + ) + } + + @Test + fun `bare-value seed emits concrete identity and abstract identity, never the mixed edge`() { + val m = mgr(false) + val abstraction = BaseOnlyInitialFactAbstraction(m) + val produced = abstraction.addAbstractedInitialFact( + BaseOnlyFinalFactAp(m, arg0, m.acc(FinalAccessor, abstract = false), ExclusionSet.Empty), + FactTypeChecker.Dummy, + ) + val concrete = m.acc(FinalAccessor, abstract = false) + val abstract = m.acc(abstract = true) + assertTrue(contains(produced, concrete, concrete), "expected prefix.\$ => prefix.\$, got $produced") + assertTrue(contains(produced, abstract, abstract), "expected prefix.* => prefix.*, got $produced") + assertNoMixedEdge(produced) + } + + @Test + fun `static-only value seed never emits a concrete-to-abstract edge`() { + val m = mgr(false) + val stat = ClassStaticAccessor("S") + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.addAbstractedInitialFact( + BaseOnlyFinalFactAp(m, arg0, m.acc(stat, FinalAccessor, abstract = false), ExclusionSet.Empty), + FactTypeChecker.Dummy, + ) + val produced = abstraction.registerNewInitialFact(m.analyzedExcluding(stat), FactTypeChecker.Dummy) + assertNoMixedEdge(produced) + val concrete = m.acc(stat, FinalAccessor, abstract = false) + assertTrue(contains(produced, concrete, concrete), "static-only terminal must emit the concrete identity, got $produced") + } + + @Test + fun `field-only value seed never emits a concrete-to-abstract edge`() { + val m = mgr(fieldSensitive = true) + val abstraction = BaseOnlyInitialFactAbstraction(m) + abstraction.addAbstractedInitialFact( + BaseOnlyFinalFactAp(m, arg0, m.acc(field, FinalAccessor, abstract = false), ExclusionSet.Empty), + FactTypeChecker.Dummy, + ) + val produced = abstraction.registerNewInitialFact(m.analyzedExcluding(field), FactTypeChecker.Dummy) + assertNoMixedEdge(produced) + val concrete = m.acc(field, FinalAccessor, abstract = false) + assertTrue(contains(produced, concrete, concrete), "field-only terminal must emit the concrete identity, got $produced") + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionDifferentialTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionDifferentialTest.kt new file mode 100644 index 000000000..6811a7540 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionDifferentialTest.kt @@ -0,0 +1,364 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import kotlinx.collections.immutable.persistentHashSetOf +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor +import org.opentaint.dataflow.util.Cancellation +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + +class BaseOnlyInitialFactAbstractionDifferentialTest { + @Test + fun `an exclusion with no matching active blocker emits nothing`() { + val manager = manager() + val indexed = BaseOnlyInitialFactAbstraction(manager) + val linear = LinearInitialFactAbstraction(manager) + val field = FieldAccessor("Owner", "blocked", "Value") + val unrelated = TaintMarkAccessor("unrelated") + + assertEquivalent( + Add(finalFact(manager, manager.interner.index(field), FINAL_ACCESSOR_IDX)), + indexed, + linear, + ) + val output = assertEquivalent( + Register(demand(manager, setOf(unrelated))), + indexed, + linear, + ) + + assertEquals(emptySet(), output) + } + + @Test + fun `only facts blocked by an added exclusion advance`() { + val manager = manager() + val indexed = BaseOnlyInitialFactAbstraction(manager) + val linear = LinearInitialFactAbstraction(manager) + val first = TaintMarkAccessor("first") + val second = TaintMarkAccessor("second") + val firstAccess = packBaseOnlyAccess( + NO_ACCESSOR, NO_ACCESSOR, manager.interner.index(first), BaseOnlyValueAccessorState.Normal, + ) + val secondAccess = packBaseOnlyAccess( + NO_ACCESSOR, NO_ACCESSOR, manager.interner.index(second), BaseOnlyValueAccessorState.Normal, + ) + + assertEquivalent(Add(finalFact(manager, access = firstAccess)), indexed, linear) + assertEquivalent(Add(finalFact(manager, access = secondAccess)), indexed, linear) + val output = assertEquivalent(Register(demand(manager, setOf(first))), indexed, linear) + + assertEquals( + setOf(EdgeKey(AccessPathBase.This, firstAccess, firstAccess)), + output, + "the unrelated second blocker must remain pending", + ) + } + + @Test + fun `type group and concrete type blocker indices cannot emit the same fact twice`() { + val manager = manager() + val indexed = BaseOnlyInitialFactAbstraction(manager) + val linear = LinearInitialFactAbstraction(manager) + val type = TypeInfoAccessor("pkg.Type") + val typeAccess = packBaseOnlyAccess( + NO_ACCESSOR, NO_ACCESSOR, manager.interner.index(type), BaseOnlyValueAccessorState.Normal, + ) + + assertEquivalent(Add(finalFact(manager, access = typeAccess)), indexed, linear) + val unblockedByGroup = assertEquivalent( + Register(demand(manager, setOf(TypeInfoGroupAccessor))), + indexed, + linear, + ) + assertEquals(setOf(EdgeKey(AccessPathBase.This, typeAccess, typeAccess)), unblockedByGroup) + + val duplicate = assertEquivalent(Register(demand(manager, setOf(type))), indexed, linear) + assertEquals(emptySet(), duplicate, "unblocking through the dual concrete index must not re-emit the fact") + } + + @Test + fun `indexed abstraction agrees with a linear rescan reference over random operation sequences`() { + repeat(SEEDS) { seed -> + val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + val indexed = BaseOnlyInitialFactAbstraction(manager) + val linear = LinearInitialFactAbstraction(manager) + val fixture = Fixture(manager) + val random = Random(seed) + + repeat(STEPS_PER_SEED) { step -> + val operation = fixture.randomOperation(random) + val expected = operation.apply(linear) + val actual = operation.apply(indexed) + assertEquals( + expected.toEdgeKeys(), + actual.toEdgeKeys(), + "seed=$seed, step=$step, operation=$operation", + ) + } + } + } + + private fun manager() = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + + private fun finalFact( + manager: BaseOnlyApManager, + fieldIdx: Int = NO_ACCESSOR, + suffixIdx: Int = FINAL_ACCESSOR_IDX, + access: BaseOnlyAccess = packBaseOnlyAccess(fieldIdx = fieldIdx, staticIdx = NO_ACCESSOR, suffixIdx = suffixIdx), + ) = BaseOnlyFinalFactAp(manager, AccessPathBase.This, access, ExclusionSet.Empty) + + private fun demand(manager: BaseOnlyApManager, exclusions: Set) = BaseOnlyInitialFactAp( + manager, + AccessPathBase.This, + ABSTRACT_EMPTY_ACCESS, + ExclusionSet.Concrete(persistentHashSetOf(*exclusions.toTypedArray())), + ) + + private fun assertEquivalent( + operation: Operation, + indexed: BaseOnlyInitialFactAbstraction, + linear: LinearInitialFactAbstraction, + ): Set { + val expected = operation.apply(linear).toEdgeKeys() + val actual = operation.apply(indexed).toEdgeKeys() + assertEquals(expected, actual, "operation=$operation") + return actual + } + + private class Fixture(private val manager: BaseOnlyApManager) { + private val bases = listOf(AccessPathBase.This, AccessPathBase.Argument(0), AccessPathBase.Argument(1)) + private val statics = List(3) { ClassStaticAccessor("Owner$it") } + private val fields = List(5) { FieldAccessor("Owner", "field$it", "Value") } + private val marks = List(5) { TaintMarkAccessor("mark$it") } + private val types = List(3) { TypeInfoAccessor("pkg.Type$it") } + private val possibleExclusions: List = + statics + fields + ElementAccessor + marks + types + TypeInfoGroupAccessor + ValueAccessor + + private val staticIndices = statics.map(manager.interner::index) + private val fieldIndices = fields.map(manager.interner::index) + private val markIndices = marks.map(manager.interner::index) + private val typeIndices = types.map(manager.interner::index) + + fun randomOperation(random: Random): Operation = + if (random.nextInt(100) < 48) randomAdd(random) else randomRegister(random) + + private fun randomAdd(random: Random): Operation { + val base = bases.random(random) + val staticIdx = if (random.nextInt(4) == 0) staticIndices.random(random) else NO_ACCESSOR + val fieldIdx = when (random.nextInt(4)) { + 0 -> fieldIndices.random(random) + 1 -> ELEMENT_ACCESSOR_IDX + else -> NO_ACCESSOR + } + val suffixIdx = when (random.nextInt(5)) { + 0 -> FINAL_ACCESSOR_IDX + 1, 2 -> markIndices.random(random) + else -> typeIndices.random(random) + } + val valueState = if ( + suffixIdx != FINAL_ACCESSOR_IDX && random.nextBoolean() + ) { + BaseOnlyValueAccessorState.Value + } else { + BaseOnlyValueAccessorState.Normal + } + val access = packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, valueState) + return Add(BaseOnlyFinalFactAp(manager, base, access, ExclusionSet.Empty)) + } + + private fun randomRegister(random: Random): Operation { + val base = bases.random(random) + val staticIdx = if (random.nextBoolean()) staticIndices.random(random) else NO_ACCESSOR + val fieldIdx = if (random.nextBoolean()) fieldIndices.random(random) else NO_ACCESSOR + val pattern = when (random.nextInt(7)) { + 0 -> ABSTRACT_EMPTY_ACCESS + 1 -> BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0) + 2 -> BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1) + 3 -> BaseOnlyAccessOps.abstractAt(staticIdx, NO_ACCESSOR, 1) + 4 -> BaseOnlyAccessOps.abstractAt(staticIdx, NO_ACCESSOR, 2) + 5 -> BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, fieldIdx, 2) + else -> BaseOnlyAccessOps.abstractAt(staticIdx, fieldIdx, 2) + } + val count = random.nextInt(1, 5) + val excluded = buildSet { + repeat(count) { add(possibleExclusions.random(random)) } + } + val exclusions = ExclusionSet.Concrete(persistentHashSetOf(*excluded.toTypedArray())) + return Register(BaseOnlyInitialFactAp(manager, base, pattern, exclusions)) + } + } + + private sealed interface Operation { + fun apply(abstraction: InitialFactAbstractionFacade): List> + fun apply(abstraction: BaseOnlyInitialFactAbstraction): List> + } + + private data class Add(val fact: BaseOnlyFinalFactAp) : Operation { + override fun apply(abstraction: InitialFactAbstractionFacade) = abstraction.add(fact) + override fun apply(abstraction: BaseOnlyInitialFactAbstraction) = + abstraction.addAbstractedInitialFact(fact, FactTypeChecker.Dummy) + } + + private data class Register(val fact: BaseOnlyInitialFactAp) : Operation { + override fun apply(abstraction: InitialFactAbstractionFacade) = abstraction.register(fact) + override fun apply(abstraction: BaseOnlyInitialFactAbstraction) = + abstraction.registerNewInitialFact(fact, FactTypeChecker.Dummy) + } + + private interface InitialFactAbstractionFacade { + fun add(fact: BaseOnlyFinalFactAp): List> + fun register(fact: BaseOnlyInitialFactAp): List> + } + + /** + * Deliberately has no blocker index. Every exclusion change rescans every added fact, making + * this a small, independent semantic oracle for the indexed implementation. + */ + private class LinearInitialFactAbstraction( + private val manager: BaseOnlyApManager, + ) : InitialFactAbstractionFacade { + private val perBase = mutableMapOf() + + private class BaseState { + val added = linkedSetOf() + val emitted = mutableSetOf() + val exclusionsByPattern = mutableMapOf>() + } + + override fun add(fact: BaseOnlyFinalFactAp): List> { + val state = perBase.getOrPut(fact.base, ::BaseState) + if (!state.added.add(fact.access)) return emptyList() + return buildList { abstract(fact.base, fact.access, state, this) } + } + + override fun register(fact: BaseOnlyInitialFactAp): List> { + val state = perBase.getOrPut(fact.base, ::BaseState) + val incoming = when (val exclusions = fact.exclusions) { + ExclusionSet.Empty -> emptySet() + ExclusionSet.Universe -> error("Unexpected universe exclusion") + is ExclusionSet.Concrete -> exclusions.set.mapTo(mutableSetOf(), manager.interner::index) + } + val known = state.exclusionsByPattern.getOrPut(fact.access) { mutableSetOf() } + if (!known.addAll(incoming)) return emptyList() + + return buildList { + state.added.forEach { access -> abstract(fact.base, access, state, this) } + } + } + + private fun abstract( + base: AccessPathBase, + added: BaseOnlyAccess, + state: BaseState, + output: MutableList>, + ) { + val prefix = mutableListOf() + val core = buildList { + if (added.staticIdx >= 0) add(added.staticIdx) + if (added.fieldIdx >= 0) add(added.fieldIdx) + if (added.hasSemanticMark && added.valueAccessorState == BaseOnlyValueAccessorState.Value) { + add(if (added.hasTypeInfoSuffix) TYPE_INFO_GROUP_ACCESSOR_IDX else VALUE_ACCESSOR_IDX) + } + if (added.suffixIdx >= 0 && added.suffixIdx != FINAL_ACCESSOR_IDX) add(added.suffixIdx) + } + + for (accessor in core) { + val apSlot = slotOfIdx(accessor) + val blockedAt = abstractAccess(prefix, apSlot) + emitIdentity(base, blockedAt, state, output) + if (!state.excludes(blockedAt, accessor)) return + prefix.add(accessor) + } + + if (added.hasAp) { + emitIdentity(base, abstractAccess(prefix, added.apSlot), state, output) + } else { + emitIdentity(base, abstractAccess(prefix, 2), state, output) + var concrete = BaseOnlyAccessOps.build( + (prefix + FINAL_ACCESSOR_IDX).toIntArray(), + isAbstract = false, + ) + if (concrete.hasSemanticMark) { + concrete = concrete.withValueAccessorState(added.valueAccessorState) + } + emitIdentity(base, concrete, state, output) + } + } + + private fun BaseState.excludes(blockedAt: BaseOnlyAccess, accessor: Int): Boolean = + exclusionsByPattern.any { (pattern, exclusions) -> + (pattern == ABSTRACT_EMPTY_ACCESS || BaseOnlyAccessOps.containsAccess(pattern, blockedAt)) && + (accessor in exclusions || + accessor.isTypeInfoAccessor() && TYPE_INFO_GROUP_ACCESSOR_IDX in exclusions) + } + + private fun abstractAccess(prefix: List, apSlot: Int): BaseOnlyAccess { + var staticIdx = NO_ACCESSOR + var fieldIdx = NO_ACCESSOR + prefix.forEach { idx -> + when { + idx.isStaticAccessor() -> staticIdx = idx + idx.isFieldAccessor() || idx == ELEMENT_ACCESSOR_IDX -> fieldIdx = idx + } + } + return BaseOnlyAccessOps.abstractAt(staticIdx, fieldIdx, apSlot) + } + + private fun emitIdentity( + base: AccessPathBase, + access: BaseOnlyAccess, + state: BaseState, + output: MutableList>, + ) { + if (!state.emitted.add(access)) return + output += BaseOnlyInitialFactAp(manager, base, access, ExclusionSet.Empty) to + BaseOnlyFinalFactAp(manager, base, access, ExclusionSet.Empty) + } + } + + private data class EdgeKey( + val base: AccessPathBase, + val initial: BaseOnlyAccess, + val final: BaseOnlyAccess, + ) + + private fun List>.toEdgeKeys(): Set = mapTo(mutableSetOf()) { (i, f) -> + i as BaseOnlyInitialFactAp + f as BaseOnlyFinalFactAp + EdgeKey(i.base, i.access, f.access) + } + + private companion object { + const val SEEDS = 64 + const val STEPS_PER_SEED = 300 + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyManagerTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyManagerTest.kt new file mode 100644 index 000000000..d81d4fd88 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyManagerTest.kt @@ -0,0 +1,142 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.persistentHashSetOf +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.util.Cancellation +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlyManagerTest { + private val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + + private object Seam : BaseOnlyFinalApAccess { + lateinit var mgr: BaseOnlyApManager + override val apManager: BaseOnlyApManager get() = mgr + } + + @Test + fun `create final ap carries final accessor`() { + val f = manager.createFinalAp(AccessPathBase.This, ExclusionSet.Empty) as BaseOnlyFinalFactAp + assertEquals(AccessPathBase.This, f.base) + assertEquals(1, f.size) + assertFalse(f.isAbstract()) + } + + @Test + fun `most abstract final ap is abstract`() { + val f = manager.mostAbstractFinalAp(AccessPathBase.This) as BaseOnlyFinalFactAp + assertTrue(f.isAbstract()) + assertEquals(0, f.size) + } + + @Test + fun `most abstract initial ap is abstract`() { + val f = manager.mostAbstractInitialAp(AccessPathBase.This) as BaseOnlyInitialFactAp + assertTrue(f.isAbstract()) + assertEquals(0, f.size) + } + + @Test + fun `create final initial ap carries final accessor`() { + val f = manager.createFinalInitialAp(AccessPathBase.This, ExclusionSet.Empty) as BaseOnlyInitialFactAp + assertEquals(1, f.size) + assertFalse(f.isAbstract()) + } + + @Test + fun `seam round trips final fact`() { + Seam.mgr = manager + val access = BaseOnlyAccessOps.abstractEmpty + val f = Seam.createFinal(AccessPathBase.This, access, ExclusionSet.Empty) + assertEquals(access, Seam.getFinalAccess(f)) + } + + @Test + fun `BaseOnly facts compact exclusions without changing their set algebra`() { + val first = TaintMarkAccessor("first") + val second = TaintMarkAccessor("second") + val third = TaintMarkAccessor("third") + val original = ExclusionSet.Concrete(persistentHashSetOf(first, second)) + + val fact = manager.createFinalAp(AccessPathBase.This, original) as BaseOnlyFinalFactAp + val compact = fact.exclusions as ExclusionSet.Concrete + + assertEquals(original, compact) + assertEquals(original.hashCode(), compact.hashCode()) + assertFalse(compact.set is PersistentSet<*>) + assertEquals( + ExclusionSet.Concrete(persistentHashSetOf(first, second, third)), + compact.add(third), + ) + assertEquals( + ExclusionSet.Concrete(second), + compact.intersect(ExclusionSet.Concrete(persistentHashSetOf(second, third))), + ) + assertEquals( + ExclusionSet.Concrete(first), + compact.subtract(ExclusionSet.Concrete(second)), + ) + + } + + @Test + fun `compact exclusion algebra agrees with persistent sets`() { + val accessors = List(5) { TaintMarkAccessor("exclusion-$it") } + fun exclusions(mask: Int): ExclusionSet = if (mask == 0) { + ExclusionSet.Empty + } else { + ExclusionSet.Concrete( + persistentHashSetOf(*accessors.filterIndexed { index, _ -> mask and (1 shl index) != 0 }.toTypedArray()) + ) + } + fun compact(exclusions: ExclusionSet): ExclusionSet = + manager.createFinalAp(AccessPathBase.This, exclusions).exclusions + + for (leftMask in 0 until (1 shl accessors.size)) { + val left = exclusions(leftMask) + val compactLeft = compact(left) + assertEquals(left, compactLeft) + assertEquals(left.hashCode(), compactLeft.hashCode()) + + accessors.forEach { accessor -> + assertEquals(left.add(accessor), compactLeft.add(accessor)) + assertEquals(left.subtract(accessor), compactLeft.subtract(accessor)) + } + + for (rightMask in 0 until (1 shl accessors.size)) { + val right = exclusions(rightMask) + val compactRight = compact(right) + assertEquals(left.union(right), compactLeft.union(compactRight)) + assertEquals(left.intersect(right), compactLeft.intersect(compactRight)) + if (compactLeft is ExclusionSet.Concrete && compactRight is ExclusionSet.Concrete) { + assertEquals( + (left as ExclusionSet.Concrete).subtract(right as ExclusionSet.Concrete), + compactLeft.subtract(compactRight), + ) + val update = + (compactLeft.set as BaseOnlyExclusionAccessorSet) + .unionWithAdded(compactRight.set as BaseOnlyExclusionAccessorSet) + val changedUnion = + (compactLeft.set as BaseOnlyExclusionAccessorSet) + .unionIfChanged(compactRight.set as BaseOnlyExclusionAccessorSet) + val expectedAdded = + (right as ExclusionSet.Concrete).subtract(left as ExclusionSet.Concrete) + if (expectedAdded is ExclusionSet.Empty) { + assertEquals(null, update) + assertEquals(null, changedUnion) + } else { + assertEquals(left.union(right), ExclusionSet.Concrete(checkNotNull(update).union)) + assertEquals(expectedAdded, ExclusionSet.Concrete(update.added)) + assertEquals(left.union(right), ExclusionSet.Concrete(checkNotNull(changedUnion))) + } + } + } + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyRelationLawTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyRelationLawTest.kt new file mode 100644 index 000000000..a778d0c2b --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyRelationLawTest.kt @@ -0,0 +1,88 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlyRelationLawTest { + private val interner = AccessorInterner() + private val stat = interner.index(ClassStaticAccessor("S")) + private val field = interner.index(FieldAccessor("A", "f", "B")) + private val otherField = interner.index(FieldAccessor("A", "g", "B")) + private val mark = interner.index(TaintMarkAccessor("m")) + private val value = interner.index(ValueAccessor) + private val any = interner.index(AnyAccessor) + private val final = interner.index(FinalAccessor) + + private fun access(vararg idx: Int, abstract: Boolean = false): BaseOnlyAccess = + BaseOnlyAccessOps.build(idx, abstract) + + private val states: List by lazy { + val normal = access(mark) + val valueSuffix = access(value, mark) + listOf( + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1), + BaseOnlyAccessOps.abstractEmpty, + BaseOnlyAccessOps.abstractAt(stat, NO_ACCESSOR, 1), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, field, 2), + normal, + valueSuffix, + access(any, mark), + access(field, mark), + access(otherField, mark), + access(final), + access(field, final), + ) + } + + @Test + fun `coverage is reflexive and transitive`() { + for (a in states) assertTrue(BaseOnlyAccessOps.covers(a, a), "not reflexive: $a") + for (a in states) for (b in states) for (c in states) { + if (BaseOnlyAccessOps.covers(a, b) && BaseOnlyAccessOps.covers(b, c)) { + assertTrue(BaseOnlyAccessOps.covers(a, c), "not transitive: $a >= $b >= $c") + } + } + } + + @Test + fun `overlap is reflexive symmetric and distinct from coverage`() { + for (a in states) { + assertTrue(BaseOnlyAccessOps.mayOverlap(a, a), "not reflexive: $a") + for (b in states) { + assertTrue( + BaseOnlyAccessOps.mayOverlap(a, b) == BaseOnlyAccessOps.mayOverlap(b, a), + "not symmetric: $a, $b", + ) + } + } + + val bareMark = access(mark) + val anyMark = access(any, mark) + val concreteFieldMark = access(field, mark) + assertTrue(BaseOnlyAccessOps.covers(bareMark, concreteFieldMark)) + assertTrue(BaseOnlyAccessOps.covers(bareMark, anyMark)) + assertTrue(BaseOnlyAccessOps.covers(anyMark, bareMark)) + assertTrue(BaseOnlyAccessOps.covers(anyMark, concreteFieldMark)) + assertFalse(BaseOnlyAccessOps.covers(concreteFieldMark, bareMark)) + assertTrue(BaseOnlyAccessOps.mayOverlap(bareMark, concreteFieldMark)) + assertTrue(BaseOnlyAccessOps.mayOverlap(bareMark, anyMark)) + assertTrue(BaseOnlyAccessOps.mayOverlap(anyMark, concreteFieldMark)) + + val normal = access(mark) + val valueSuffix = access(value, mark) + val joined = canonicalJoin(normal, valueSuffix) + assertTrue(joined == setOf(normal, valueSuffix)) + assertFalse(BaseOnlyAccessOps.covers(normal, valueSuffix)) + assertFalse(BaseOnlyAccessOps.covers(valueSuffix, normal)) + assertFalse(BaseOnlyAccessOps.mayOverlap(normal, valueSuffix)) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializerTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializerTest.kt new file mode 100644 index 000000000..7922d0e32 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializerTest.kt @@ -0,0 +1,200 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext +import org.opentaint.ir.api.common.CommonMethod +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class BaseOnlySerializerTest { + private val arg0 = AccessPathBase.Argument(0) + private val field = FieldAccessor("A", "f", "B") + private val mark = TaintMarkAccessor("m") + private val stat = ClassStaticAccessor("A") + + private val m = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + private val context = InMemoryContext() + private val serializer = m.createSerializer(context) + + private fun BaseOnlyApManager.finalOf(exclusions: ExclusionSet, vararg accessors: Accessor): FinalFactAp { + var f = createFinalAp(arg0, exclusions) + accessors.reversed().forEach { f = f.prependAccessor(it) } + return f + } + + private fun roundTripFinal(ap: FinalFactAp): FinalFactAp { + val encoded = encodeFinal(ap) + return decodeFinal(encoded) + } + + private fun encodeFinal(ap: FinalFactAp): ByteArray { + val bytes = ByteArrayOutputStream() + DataOutputStream(bytes).use { out -> with(serializer) { out.writeFinalAp(ap) } } + return bytes.toByteArray() + } + + private fun decodeFinal(encoded: ByteArray): FinalFactAp = + DataInputStream(ByteArrayInputStream(encoded)).use { input -> + with(serializer) { input.readFinalAp() } + } + + private fun roundTripInitial(ap: InitialFactAp): InitialFactAp { + val bytes = ByteArrayOutputStream() + DataOutputStream(bytes).use { out -> with(serializer) { out.writeInitialAp(ap) } } + return DataInputStream(ByteArrayInputStream(bytes.toByteArray())).use { input -> + with(serializer) { input.readInitialAp() } + } + } + + @Test + fun `round trips a final fact with any and mark`() { + val ap = m.finalOf(ExclusionSet.Empty, field, AnyAccessor, mark) + assertEquals(ap, roundTripFinal(ap)) + } + + @Test + fun `Any is implicit and is not serialized in the field slot`() { + val anyMark = m.finalOf(ExclusionSet.Empty, AnyAccessor, mark) + val restored = roundTripFinal(anyMark) + + assertEquals(anyMark, restored) + assertEquals(NO_ACCESSOR, (restored as BaseOnlyFinalFactAp).access.fieldIdx) + assertEquals(setOf(AnyAccessor, mark), restored.getStartAccessors()) + assertTrue(restored.startsWithAccessor(mark)) + assertNotNull(restored.readAccessor(AnyAccessor)) + } + + @Test + fun `round trips an abstract final fact`() { + val ap = m.mostAbstractFinalAp(arg0) + assertEquals(ap, roundTripFinal(ap)) + } + + @Test + fun `round trips a final fact with concrete exclusions`() { + val ap = m.finalOf(ExclusionSet.Empty, AnyAccessor, mark).exclude(field) + assertEquals(ap, roundTripFinal(ap)) + } + + @Test + fun `round trips a final fact with a collapsed type pair`() { + val ap = m.finalOf(ExclusionSet.Empty, TypeInfoGroupAccessor, TypeInfoAccessor("pkg.fn")) + assertEquals(ap, roundTripFinal(ap)) + } + + @Test + fun `round trips normal and value states for taint and type terminals`() { + val terminals = listOf( + m.finalOf(ExclusionSet.Empty, mark) as BaseOnlyFinalFactAp, + m.finalOf(ExclusionSet.Empty, ValueAccessor, mark) as BaseOnlyFinalFactAp, + m.finalOf(ExclusionSet.Empty, TypeInfoAccessor("pkg.direct")) as BaseOnlyFinalFactAp, + m.finalOf( + ExclusionSet.Empty, TypeInfoGroupAccessor, TypeInfoAccessor("pkg.wrapped"), + ) as BaseOnlyFinalFactAp, + ) + for (expected in terminals) { + val restored = roundTripFinal(expected) as BaseOnlyFinalFactAp + assertEquals(expected, restored) + assertEquals(expected.access.valueAccessorState, restored.access.valueAccessorState) + } + } + + @Test + fun `deserializer rejects a lone value wrapper as a terminal`() { + val localContext = InMemoryContext() + val localSerializer = m.createSerializer(localContext) + val direct = m.finalOf(ExclusionSet.Empty, mark) + val bytes = ByteArrayOutputStream() + DataOutputStream(bytes).use { out -> with(localSerializer) { out.writeFinalAp(direct) } } + val markId = localContext.getIdByAccessor(mark) + localContext.replaceAccessor(markId, ValueAccessor) + assertFailsWith { + DataInputStream(ByteArrayInputStream(bytes.toByteArray())).use { input -> + with(localSerializer) { input.readFinalAp() } + } + } + } + + @Test + fun `round trips an initial fact with final accessor`() { + val ap = m.createFinalInitialAp(arg0, ExclusionSet.Empty).prependAccessor(mark).prependAccessor(AnyAccessor) + assertEquals(ap, roundTripInitial(ap)) + } + + @Test + fun `round trips every abstraction slot without rebuilding the path and rejects transient collapsed state`() { + val statIdx = m.interner.index(stat) + val fieldIdx = m.interner.index(field) + val accesses = listOf( + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0), + BaseOnlyAccessOps.abstractAt(statIdx, NO_ACCESSOR, 1), + BaseOnlyAccessOps.abstractAt(statIdx, fieldIdx, 2), + ) + + for (access in accesses) { + val final = BaseOnlyFinalFactAp(m, arg0, access, ExclusionSet.Empty) + val initial = BaseOnlyInitialFactAp(m, arg0, access, ExclusionSet.Empty) + assertEquals(final, roundTripFinal(final), "final access $access") + assertEquals(initial, roundTripInitial(initial), "initial access $access") + } + + val transient = BaseOnlyFinalFactAp( + m, + arg0, + BaseOnlyAccessOps.collapse(BaseOnlyAccessOps.abstractAt(statIdx, fieldIdx, 2)), + ExclusionSet.Empty, + ) + assertFailsWith { + roundTripFinal(transient) + } + } + + private class InMemoryContext : SummarySerializationContext { + private val accessorToId = HashMap() + private val idToAccessor = HashMap() + + override fun getIdByAccessor(accessor: Accessor): Long = + accessorToId.getOrPut(accessor) { + val id = accessorToId.size.toLong() + idToAccessor[id] = accessor + id + } + + override fun getAccessorById(id: Long): Accessor = idToAccessor.getValue(id) + + fun replaceAccessor(id: Long, accessor: Accessor) { + idToAccessor[id] = accessor + } + + override fun getIdByMethod(method: CommonMethod): Long = error("not used") + override fun getMethodById(id: Long): CommonMethod = error("not used") + override fun loadSummaries(method: CommonMethod): ByteArray? = error("not used") + override fun storeSummaries(method: CommonMethod, summaries: ByteArray) = error("not used") + override fun flush() = error("not used") + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTrackerTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTrackerTest.kt new file mode 100644 index 000000000..3253586d1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTrackerTest.kt @@ -0,0 +1,89 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import kotlinx.collections.immutable.persistentHashSetOf +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.util.Cancellation +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class BaseOnlySideEffectRequirementDeltaTrackerTest { + private val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + private val first = TaintMarkAccessor("first") + private val second = TaintMarkAccessor("second") + + private fun fact( + access: BaseOnlyAccess = ABSTRACT_EMPTY_ACCESS, + exclusions: ExclusionSet = ExclusionSet.Empty, + ): InitialFactAp = BaseOnlyInitialFactAp(manager, AccessPathBase.This, access, exclusions) + + private fun exclusions(vararg marks: TaintMarkAccessor): ExclusionSet = + ExclusionSet.Concrete(persistentHashSetOf(*marks)) + + @Test + fun `first requirement is retained including an empty exclusion`() { + val tracker = BaseOnlySideEffectRequirementDeltaTracker() + val current = fact() + val requirement = fact() + + assertEquals(requirement, tracker.add(current, requirement)) + assertNull(tracker.add(current, requirement)) + } + + @Test + fun `growing requirement publishes only newly added exclusions`() { + val tracker = BaseOnlySideEffectRequirementDeltaTracker() + val current = fact() + + assertEquals(exclusions(first), tracker.add(current, fact(exclusions = exclusions(first)))?.exclusions) + assertEquals( + exclusions(second), + tracker.add(current, fact(exclusions = exclusions(first, second)))?.exclusions, + ) + assertNull(tracker.add(current, fact(exclusions = exclusions(first, second)))) + } + + @Test + fun `different access operations keep independent exclusion state`() { + val tracker = BaseOnlySideEffectRequirementDeltaTracker() + val current = fact() + val otherAccess = manager.finalAccessorAccess + + assertEquals(exclusions(first), tracker.add(current, fact(exclusions = exclusions(first)))?.exclusions) + assertEquals( + exclusions(first), + tracker.add(current, fact(otherAccess, exclusions(first)))?.exclusions, + ) + } + + @Test + fun `universe is published once after a concrete exclusion`() { + val tracker = BaseOnlySideEffectRequirementDeltaTracker() + val current = fact() + + assertEquals(exclusions(first), tracker.add(current, fact(exclusions = exclusions(first)))?.exclusions) + assertEquals( + ExclusionSet.Universe, + tracker.add(current, fact(exclusions = ExclusionSet.Universe))?.exclusions, + ) + assertNull(tracker.add(current, fact(exclusions = exclusions(first, second)))) + assertNull(tracker.add(current, fact(exclusions = ExclusionSet.Universe))) + } + + @Test + fun `empty state accepts the first later concrete exclusion`() { + val tracker = BaseOnlySideEffectRequirementDeltaTracker() + val current = fact() + + assertEquals(ExclusionSet.Empty, tracker.add(current, fact())?.exclusions) + assertEquals(exclusions(first), tracker.add(current, fact(exclusions = exclusions(first)))?.exclusions) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySplitDeltaAlignmentTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySplitDeltaAlignmentTest.kt new file mode 100644 index 000000000..060bdef42 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySplitDeltaAlignmentTest.kt @@ -0,0 +1,238 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals + +// Pins that split-delta's field handling is aligned with contains: every (final ⊇ initial) +// pair under the symmetric field-[any] `contains` yields a NON-EMPTY splitDelta (never dropped +// to NO-MATCH), and no non-contained pair yields an ε residual. See +// docs/superpowers/specs/2026-07-10-baseonly-split-delta-alignment-design.md +class BaseOnlySplitDeltaAlignmentTest { + private val base = AccessPathBase.Argument(0) + + private val s1 = ClassStaticAccessor("S1") + private val s2 = ClassStaticAccessor("S2") + private val f1 = FieldAccessor("C", "f1", "T") + private val f2 = FieldAccessor("C", "f2", "T") + private val t1 = TaintMarkAccessor("t1") + private val t2 = TaintMarkAccessor("t2") + + private enum class Suffix { ABSTRACT, VALUE, MARK1, MARK2 } + + private fun mgr(fieldSensitive: Boolean) = + BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, org.opentaint.dataflow.util.Cancellation(), fieldSensitive = fieldSensitive) + + private fun BaseOnlyApManager.mkAccess(staticIdx: Int, fieldIdx: Int, suffix: Suffix): BaseOnlyAccess { + val idxs = ArrayList(3) + if (staticIdx != NO_ACCESSOR) idxs.add(staticIdx) + if (fieldIdx != NO_ACCESSOR) idxs.add(fieldIdx) + var isAbstract = false + when (suffix) { + Suffix.ABSTRACT -> isAbstract = true + Suffix.VALUE -> idxs.add(FINAL_ACCESSOR_IDX) + Suffix.MARK1 -> idxs.add(interner.index(t1)) + Suffix.MARK2 -> idxs.add(interner.index(t2)) + } + return BaseOnlyAccessOps.build(idxs.toIntArray(), isAbstract) + } + + private fun BaseOnlyApManager.statics(): List = + listOf(NO_ACCESSOR, interner.index(s1), interner.index(s2)) + + private fun BaseOnlyApManager.fields(): List = + if (fieldSensitive) listOf(NO_ACCESSOR, interner.index(f1), interner.index(f2), ELEMENT_ACCESSOR_IDX) + else listOf(NO_ACCESSOR) + + private fun BaseOnlyApManager.facts(): List { + val out = LinkedHashSet() + for (st in statics()) for (fl in fields()) { + for (sf in Suffix.values()) out.add(mkAccess(st, fl, sf)) + } + out.add(BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0)) + for (st in statics()) out.add(BaseOnlyAccessOps.abstractAt(st, NO_ACCESSOR, 1)) + return out.toList() + } + + private fun BaseOnlyApManager.label(idx: Int): String = when (idx) { + interner.index(s1) -> "s1" + interner.index(s2) -> "s2" + interner.index(f1) -> "f1" + interner.index(f2) -> "f2" + interner.index(t1) -> "t1" + interner.index(t2) -> "t2" + ELEMENT_ACCESSOR_IDX -> "[el]" + else -> "#$idx" + } + + private fun BaseOnlyApManager.render(a: BaseOnlyAccess, root: String): String { + val sb = StringBuilder(root) + when { + a.staticIdx == ABSTRACT_MARK -> sb.append(".*s") + a.staticIdx >= 0 -> sb.append(".").append(label(a.staticIdx)) + } + when { + a.fieldIdx == ABSTRACT_MARK -> sb.append(".*f") + a.fieldIdx >= 0 -> sb.append(".").append(label(a.fieldIdx)) + } + if (a.suffixIdx >= 0) { + if (a.hasSemanticMark) sb.append(".!").append(label(a.suffixIdx)) + sb.append(".$") + } + if (a.isSuffixAbstract) sb.append(".*") + return sb.toString() + } + + // classify splitDelta(initial, final): "NM" | "ε" | "Δ..." (+ε if both), and whether it has an ε entry. + private fun BaseOnlyApManager.splitPairs(final: BaseOnlyAccess, initial: BaseOnlyAccess): List> { + val initAp = BaseOnlyInitialFactAp(this, base, initial, ExclusionSet.Empty) + val finalAp = BaseOnlyFinalFactAp(this, base, final, ExclusionSet.Empty) + return initAp.splitDelta(finalAp).map { (matched, delta) -> + (matched as BaseOnlyInitialFactAp).access to (delta as BaseOnlyInitialDelta) + } + } + + private fun BaseOnlyApManager.renderSplit(final: BaseOnlyAccess, initial: BaseOnlyAccess): String { + val pairs = splitPairs(final, initial) + if (pairs.isEmpty()) return "NM" + return pairs.joinToString("+") { (mAccess, delta) -> + val d = when (delta) { + BaseOnlyEmptyInitialDelta -> "ε" + is BaseOnlyNodeInitialDelta -> render(delta.access, "Δ") + } + if (mAccess == initial) d else "[${render(mAccess, "m")}]$d" + } + } + + private fun splitHasEmpty(pairs: List>): Boolean = + pairs.any { it.second === BaseOnlyEmptyInitialDelta } + + // per-pair alignment symbol + private fun BaseOnlyApManager.sym(final: BaseOnlyAccess, initial: BaseOnlyAccess): String { + val c = BaseOnlyFinalFactAp(this, base, final, ExclusionSet.Empty) + .contains(BaseOnlyInitialFactAp(this, base, initial, ExclusionSet.Empty)) + val pairs = splitPairs(final, initial) + val any = pairs.isNotEmpty() + val eps = splitHasEmpty(pairs) + return when { + c && eps -> "e" // contained, ε residual (aligned) + c && any -> "d" // contained, structural Δ residual (matched, acceptable) + c && !any -> "X" // contained but DROPPED (misalignment / FN risk) + !c && eps -> "S" // not contained but ε (over-match) + !c && any -> ":" // not contained, structural residual (normal extension) + else -> "." // not contained, no match + } + } + + private fun dump(m: BaseOnlyApManager): String { + val sb = StringBuilder() + val facts = m.facts() + val labels = facts.map { m.render(it, "x") } + + sb.appendLine("================================================================") + sb.appendLine("BASE-ONLY split-delta vs contains ALIGNMENT PIN — fieldSensitive=${m.fieldSensitive}") + sb.appendLine("cell (final=row, initial=col):") + sb.appendLine(" . not contained, no match : not contained, structural residual") + sb.appendLine(" e contained, ε residual (aligned) d contained, structural Δ residual (matched)") + sb.appendLine(" X contained but DROPPED (misalign) S not contained but ε (over-match)") + sb.appendLine("Alignment invariant: no X, no S.") + sb.appendLine("================================================================") + sb.appendLine() + + sb.appendLine("## FACTS (${facts.size})") + facts.forEachIndexed { i, a -> + val tag = when { + a.hasAp -> "ap@${a.apSlot}" + a.hasSemanticMark -> "mark" + a.suffixIdx == FINAL_ACCESSOR_IDX -> "value" + else -> "open" + } + sb.appendLine(" F%02d = %-16s (%2d,%2d,%2d) [%s]".format(i, labels[i], a.staticIdx, a.fieldIdx, a.suffixIdx, tag)) + } + sb.appendLine() + + val grid = Array(facts.size) { fi -> Array(facts.size) { ii -> m.sym(facts[fi], facts[ii]) } } + + sb.appendLine("## ALIGNMENT MATRIX") + sb.append(" ") + for (ii in facts.indices) sb.append("%-4s".format("F%02d".format(ii))) + sb.appendLine() + for (fi in facts.indices) { + sb.append(" F%02d ".format(fi)) + for (ii in facts.indices) sb.append("%-4s".format(grid[fi][ii])) + sb.appendLine() + } + sb.appendLine() + + val counts = LinkedHashMap() + for (fi in facts.indices) for (ii in facts.indices) counts.merge(grid[fi][ii], 1, Int::plus) + sb.appendLine("## SUMMARY (symbol counts)") + for ((k, v) in counts) sb.appendLine(" '$k' : $v") + sb.appendLine() + + sb.appendLine("## CONTAINMENT PAIRS (contained, off-diagonal) — residual per pair") + sb.appendLine(" final initial | sym | splitDelta(i,f)") + for (fi in facts.indices) for (ii in facts.indices) { + if (fi == ii) continue + val s = grid[fi][ii] + if (s != "e" && s != "d" && s != "X") continue + sb.appendLine(" %-15s %-15s | %s | %s".format(labels[fi], labels[ii], s, m.renderSplit(facts[fi], facts[ii]))) + } + sb.appendLine() + return sb.toString() + } + + private fun assertAligned(m: BaseOnlyApManager) { + val facts = m.facts() + val dropped = ArrayList() + val overMatch = ArrayList() + for (fi in facts.indices) for (ii in facts.indices) { + when (m.sym(facts[fi], facts[ii])) { + "X" -> dropped.add("${m.render(facts[fi], "x")} ⊇ ${m.render(facts[ii], "x")}") + "S" -> overMatch.add("${m.render(facts[fi], "x")} !⊇ ${m.render(facts[ii], "x")} but ε") + } + } + assertEquals(emptyList(), dropped, "contained pairs dropped by split-delta (fieldSensitive=${m.fieldSensitive})") + assertEquals(emptyList(), overMatch, "non-contained pairs producing ε (fieldSensitive=${m.fieldSensitive})") + } + + private fun pin(mode: Int) { + val m = mgr(mode >= 1) + val actual = dump(m) + val scratch = File("/tmp/claude-1002/-drive-testcomp-opentaint-go-rules-opentaint/597d4672-dd12-411f-bbdb-d64b06ae40cd/scratchpad/splitdelta_align_mode$mode.txt") + scratch.parentFile.mkdirs() + scratch.writeText(actual) + val golden = javaClass.getResource("/baseonly/splitdelta_align_mode$mode.golden.txt") + if (golden == null) { + println("PIN splitdelta-align mode$mode: no golden resource yet — wrote actual to ${scratch.path}") + } else { + fun String.normalizeLineEnds(): String = + lineSequence().joinToString("\n") { it.trimEnd() }.trimEnd() + assertEquals( + golden.readText().normalizeLineEnds(), + actual.normalizeLineEnds(), + "split-delta alignment behaviour changed for mode $mode", + ) + } + } + + @Test + fun `pin mode0`() = pin(0) + + @Test + fun `pin mode1`() = pin(1) + + @Test + fun `split-delta is aligned with contains - mode0`() = assertAligned(mgr(false)) + + @Test + fun `split-delta is aligned with contains - mode1`() = assertAligned(mgr(true)) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySubscriptionAndReqTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySubscriptionAndReqTest.kt new file mode 100644 index 000000000..397202c4b --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySubscriptionAndReqTest.kt @@ -0,0 +1,496 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.Edge +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.SideEffectKind +import org.opentaint.dataflow.ap.ifds.SideEffectSummary +import org.opentaint.dataflow.ap.ifds.SummaryEdgeStorageWithSubscribers +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.SummaryEdgeSubscriptionManager.FactEdgeSummarySubscription +import org.opentaint.dataflow.ap.ifds.SummaryEdgeSubscriptionManager.FactNDEdgeSummarySubscription +import org.opentaint.dataflow.ap.ifds.SummaryEdgeSubscriptionManager.ZeroEdgeSummarySubscription +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.ApManager +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class BaseOnlySubscriptionAndReqTest { + private val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + private val fieldA = FieldAccessor("Owner", "a", "Value") + private val fieldB = FieldAccessor("Owner", "b", "Value") + private val mark = TaintMarkAccessor("m") + private val entryPoint by lazy { MethodEntryPoint(EmptyMethodContext, inst) } + + private val method = object : CommonMethod { + override val name: String = "baseOnlySubscription" + override val parameters: List = listOf(object : CommonMethodParameter { + override val type: CommonTypeName = object : CommonTypeName { + override val typeName: String = "java.lang.Object" + } + }) + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + + private val inst = object : CommonInst { + override fun toString(): String = "i0" + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod get() = this@BaseOnlySubscriptionAndReqTest.method + } + } + + private fun pattern(field: FieldAccessor): BaseOnlyAccess = + packBaseOnlyAccess(NO_ACCESSOR, manager.interner.index(field), ABSTRACT_MARK) + + private fun marked(field: FieldAccessor): BaseOnlyAccess = + packBaseOnlyAccess(NO_ACCESSOR, manager.interner.index(field), manager.interner.index(mark)) + + private fun initial( + access: BaseOnlyAccess, + base: AccessPathBase = AccessPathBase.This, + ): BaseOnlyInitialFactAp = BaseOnlyInitialFactAp(manager, base, access, ExclusionSet.Empty) + + private fun final( + access: BaseOnlyAccess, + base: AccessPathBase = AccessPathBase.Return, + ): BaseOnlyFinalFactAp = BaseOnlyFinalFactAp(manager, base, access, ExclusionSet.Universe) + + @Test + fun `summary storage publishes only non-empty deltas`() { + val storage = SummaryEdgeStorageWithSubscribers(manager, entryPoint) + val summaryDeltas = mutableListOf>() + val requirementDeltas = mutableListOf>() + val sideEffectDeltas = mutableListOf>() + storage.subscribeOnEdges(object : SummaryEdgeStorageWithSubscribers.Subscriber { + override fun newSummaryEdges(edges: List) { + summaryDeltas.add(edges) + } + + override fun newSideEffectRequirement( + methodEntryPoint: MethodEntryPoint, + requirements: List, + ) { + requirementDeltas.add(requirements) + } + + override fun newSideEffectSummaries( + methodEntryPoint: MethodEntryPoint, + sideEffects: List, + ) { + sideEffectDeltas.add(sideEffects) + } + }) + + storage.addEdges(emptyList()) + storage.sideEffectRequirement(emptyList()) + storage.addSideEffectSummaries(emptyList()) + assertTrue(summaryDeltas.isEmpty()) + assertTrue(requirementDeltas.isEmpty()) + assertTrue(sideEffectDeltas.isEmpty()) + + val edge = Edge.FactToFact( + entryPoint, + initial(pattern(fieldA)), + inst, + BaseOnlyFinalFactAp(manager, AccessPathBase.Return, marked(fieldA), ExclusionSet.Empty), + ) + storage.addEdges(listOf(edge)) + assertEquals(1, summaryDeltas.size) + assertEquals(1, requirementDeltas.size) + + storage.addEdges(listOf(edge)) + assertEquals(1, summaryDeltas.size, "a subsumed edge has no publication delta") + assertEquals(1, requirementDeltas.size, "a subsumed requirement has no publication delta") + + val sideEffect = SideEffectSummary.ZeroSideEffectSummary(object : SideEffectKind {}) + storage.addSideEffectSummaries(listOf(sideEffect)) + assertEquals(1, sideEffectDeltas.size) + + storage.addSideEffectSummaries(listOf(sideEffect)) + assertEquals(1, sideEffectDeltas.size, "a duplicate side effect has no publication delta") + } + + @Test + fun `fact subscription indexes applicable and empty delta candidates`() { + val sub = manager.accessPathSubscription() + val callerInitial = initial(pattern(fieldA)) + val exactExit = final(pattern(fieldA)) + val extendedExit = final(marked(fieldA)) + val unrelatedExit = final(marked(fieldB)) + + assertNotNull(sub.addFactToFact(inst, AccessPathBase.This, callerInitial, exactExit)) + assertNotNull(sub.addFactToFact(inst, AccessPathBase.This, callerInitial, extendedExit)) + assertNotNull(sub.addFactToFact(inst, AccessPathBase.This, callerInitial, unrelatedExit)) + assertNull(sub.addFactToFact(inst, AccessPathBase.This, callerInitial, extendedExit)) + + val summaryInitial = initial(pattern(fieldA)) + val applicable = mutableListOf() + sub.collectFactEdge(applicable, summaryInitial, emptyDeltaRequired = false) + assertEquals(2, applicable.size, "identity and non-empty delta candidates are applicable") + + val empty = mutableListOf() + sub.collectFactEdge(empty, summaryInitial, emptyDeltaRequired = true) + assertEquals(2, empty.size, "empty-delta mode uses the same conservative candidates") + } + + @Test + fun `fact subscription preserves exclusion-distinct registrations`() { + val sub = manager.accessPathSubscription() + val access = pattern(fieldA) + val exit = final(marked(fieldA)) + val first = initial(access).replaceExclusions(ExclusionSet.Empty.add(fieldA)) + val expanded = first.replaceExclusions(first.exclusions.add(fieldB)) + + assertNotNull(sub.addFactToFact(inst, AccessPathBase.This, first, exit)) + assertNotNull(sub.addFactToFact(inst, AccessPathBase.This, expanded, exit)) + assertNull(sub.addFactToFact(inst, AccessPathBase.This, first, exit)) + + val collected = mutableListOf() + sub.collectFactEdge(collected, initial(access), emptyDeltaRequired = false) + + val retained = collected.map { it.setStatements(entryPoint, inst).callerPathEdge }.toSet() + assertEquals(setOf(first, expanded), retained.mapTo(hashSetOf()) { it.initialFactAp }) + assertEquals(setOf(first.exclusions, expanded.exclusions), retained.mapTo(hashSetOf()) { it.factAp.exclusions }) + } + + @Test + fun `zero subscription indexes applicable candidates`() { + val sub = manager.accessPathSubscription() + sub.addZeroToFact(inst, AccessPathBase.This, final(pattern(fieldA))) + sub.addZeroToFact(inst, AccessPathBase.This, final(marked(fieldA))) + sub.addZeroToFact(inst, AccessPathBase.This, final(marked(fieldB))) + + val collected = mutableListOf() + sub.collectZeroEdge(collected, initial(pattern(fieldA))) + assertEquals(2, collected.size, "identity and non-empty delta candidates are applicable") + } + + @Test + fun `ND subscription indexes applicable candidates for both residual modes`() { + val sub = manager.accessPathSubscription() + val callerInitial = setOf( + initial(pattern(fieldA)).replaceExclusions(ExclusionSet.Universe), + initial(pattern(fieldB), AccessPathBase.Argument(0)).replaceExclusions(ExclusionSet.Universe), + ) + sub.addNDFactToFact(inst, AccessPathBase.This, callerInitial, final(pattern(fieldA))) + sub.addNDFactToFact(inst, AccessPathBase.This, callerInitial, final(marked(fieldA))) + sub.addNDFactToFact(inst, AccessPathBase.This, callerInitial, final(marked(fieldB))) + + val nonEmpty = mutableListOf() + sub.collectFactNDEdge(nonEmpty, initial(pattern(fieldA)), emptyDeltaRequired = false) + assertEquals(2, nonEmpty.size) + + val empty = mutableListOf() + sub.collectFactNDEdge(empty, initial(pattern(fieldA)), emptyDeltaRequired = true) + assertEquals(2, empty.size) + } + + @Test + fun `ND subscription normalizes caller initial exclusions to Universe`() { + val sub = manager.accessPathSubscription() + val access = pattern(fieldA) + val emptyInitial = setOf(initial(access)) + val universeInitial = setOf(initial(access).replaceExclusions(ExclusionSet.Universe)) + val exit = final(marked(fieldA)) + + assertNotNull(sub.addNDFactToFact(inst, AccessPathBase.This, emptyInitial, exit)) + assertNull( + sub.addNDFactToFact(inst, AccessPathBase.This, universeInitial, exit), + "exclusions are not part of an ND subscription identity", + ) + + val collected = mutableListOf() + sub.collectFactNDEdge(collected, initial(access), emptyDeltaRequired = false) + assertEquals(1, collected.size) + } + + @Test + fun `fact subscription index equals BaseOnly delta scan`() { + val exits = listOf( + pattern(fieldA), + marked(fieldA), + marked(fieldB), + packBaseOnlyAccess(NO_ACCESSOR, manager.interner.index(fieldA), manager.finalAccessorAccess.suffixIdx), + ) + val summaryAccess = pattern(fieldA) + val callerInitial = initial(pattern(fieldA)) + val ndInitial = setOf( + callerInitial.replaceExclusions(ExclusionSet.Universe), + initial(pattern(fieldB), AccessPathBase.Argument(0)).replaceExclusions(ExclusionSet.Universe), + ) + val sub = manager.accessPathSubscription() + exits.forEach { exit -> + sub.addFactToFact(inst, AccessPathBase.This, callerInitial, final(exit)) + sub.addNDFactToFact(inst, AccessPathBase.This, ndInitial, final(exit)) + } + + val applicable = mutableListOf() + sub.collectFactEdge(applicable, initial(summaryAccess), emptyDeltaRequired = false) + val expectedApplicable = exits.count { exit -> + val match = BaseOnlyAccessOps.matchPrefix(exit, summaryAccess) + match.emptyDelta || match.hasSuffix + } + assertEquals(expectedApplicable, applicable.size) + + val empty = mutableListOf() + sub.collectFactEdge(empty, initial(summaryAccess), emptyDeltaRequired = true) + assertEquals(expectedApplicable, empty.size) + + val ndResult = mutableListOf() + sub.collectFactNDEdge(ndResult, initial(summaryAccess), emptyDeltaRequired = false) + assertEquals(expectedApplicable, ndResult.size) + } + + @Test + fun `fact subscription index equals matchPrefix for all canonical shapes`() { + val static = manager.interner.index(ClassStaticAccessor("Owner")) + val fieldAIdx = manager.interner.index(fieldA) + val fieldBIdx = manager.interner.index(fieldB) + val markIdx = manager.interner.index(mark) + val accesses = listOf( + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1), + BaseOnlyAccessOps.abstractAt(static, NO_ACCESSOR, 1), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 2), + BaseOnlyAccessOps.abstractAt(static, NO_ACCESSOR, 2), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, fieldAIdx, 2), + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, markIdx), + packBaseOnlyAccess(NO_ACCESSOR, fieldAIdx, markIdx), + packBaseOnlyAccess(NO_ACCESSOR, fieldBIdx, markIdx), + packBaseOnlyAccess(static, NO_ACCESSOR, markIdx), + packBaseOnlyAccess(static, fieldAIdx, markIdx), + ) + val sub = manager.accessPathSubscription() + val callerInitial = initial(pattern(fieldA)) + accesses.forEach { exit -> + assertNotNull(sub.addFactToFact(inst, AccessPathBase.This, callerInitial, final(exit))) + } + + accesses.forEach { summaryAccess -> + val applicable = mutableListOf() + sub.collectFactEdge(applicable, initial(summaryAccess), emptyDeltaRequired = false) + val expectedApplicable = accesses.count { exit -> + BaseOnlyAccessOps.matchPrefix(exit, summaryAccess).let { it.emptyDelta || it.hasSuffix } + } + assertEquals(expectedApplicable, applicable.size, "applicable lookup for $summaryAccess") + + val empty = mutableListOf() + sub.collectFactEdge(empty, initial(summaryAccess), emptyDeltaRequired = true) + assertEquals( + expectedApplicable, + empty.size, + "empty-delta mode uses the same conservative candidates for $summaryAccess", + ) + } + } + + @Test + fun `zero subscription index equals matchPrefix for all canonical shapes`() { + val static = manager.interner.index(ClassStaticAccessor("Owner")) + val fieldAIdx = manager.interner.index(fieldA) + val fieldBIdx = manager.interner.index(fieldB) + val markIdx = manager.interner.index(mark) + val accesses = listOf( + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 0), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 1), + BaseOnlyAccessOps.abstractAt(static, NO_ACCESSOR, 1), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, NO_ACCESSOR, 2), + BaseOnlyAccessOps.abstractAt(static, NO_ACCESSOR, 2), + BaseOnlyAccessOps.abstractAt(NO_ACCESSOR, fieldAIdx, 2), + packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, markIdx), + packBaseOnlyAccess(NO_ACCESSOR, fieldAIdx, markIdx), + packBaseOnlyAccess(NO_ACCESSOR, fieldBIdx, markIdx), + packBaseOnlyAccess(static, NO_ACCESSOR, markIdx), + packBaseOnlyAccess(static, fieldAIdx, markIdx), + ) + val sub = manager.accessPathSubscription() + accesses.forEach { exit -> + assertNotNull( + sub.addZeroToFact( + inst, + AccessPathBase.ClassStatic, + final(exit, AccessPathBase.ClassStatic), + ) + ) + } + + accesses.forEach { summaryAccess -> + val actual = mutableListOf() + sub.collectZeroEdge(actual, initial(summaryAccess, AccessPathBase.ClassStatic)) + val expected = accesses.count { exit -> + BaseOnlyAccessOps.matchPrefix(exit, summaryAccess).let { it.emptyDelta || it.hasSuffix } + } + assertEquals(expected, actual.size, "applicable lookup for $summaryAccess") + } + } + + @Test + fun `side effect requirement filters same-base entries by overlap`() { + val storage = manager.sideEffectRequirementApStorage() + val requirementA = initial(pattern(fieldA)) + val requirementB = initial(pattern(fieldB)) + + assertEquals(2, storage.add(listOf(requirementA, requirementB)).size) + assertTrue(storage.add(listOf(requirementA)).isEmpty(), "same requirement is subsumed") + + val matching = mutableListOf() + storage.filterTo(matching, final(marked(fieldA), AccessPathBase.This)) + assertEquals( + listOf(requirementA), + matching, + "same-base field-B requirement must not be broadcast", + ) + + val otherBase = mutableListOf() + storage.filterTo(otherBase, final(marked(fieldA), AccessPathBase.Return)) + assertTrue(otherBase.isEmpty(), "no requirement exists for the unrelated base") + + val all = mutableListOf() + storage.collectAllRequirementsTo(all) + assertEquals(setOf(requirementA, requirementB), all.toSet()) + } + + + @Test + fun `side effect requirement publishes exclusion delta and retains the union`() { + val storage = manager.sideEffectRequirementApStorage() + val access = pattern(fieldA) + val first = BaseOnlyInitialFactAp( + manager, + AccessPathBase.This, + access, + ExclusionSet.Empty.add(fieldA), + ) + val expanded = first.replaceExclusions(first.exclusions.add(fieldB)) + + assertEquals(listOf(first), storage.add(listOf(first))) + + val delta = storage.add(listOf(expanded)) + assertEquals(1, delta.size) + assertEquals(ExclusionSet.Empty.add(fieldB), delta.single().exclusions) + + val retained = mutableListOf() + storage.collectAllRequirementsTo(retained) + assertEquals(listOf(expanded), retained) + } + + @Test + fun `side effect requirement filtering equals a scan reference`() { + val storage = manager.sideEffectRequirementApStorage() + val requirements = listOf( + initial(pattern(fieldA)), + initial(pattern(fieldB)), + initial(ABSTRACT_EMPTY_ACCESS), + ) + storage.add(requirements) + + val facts = listOf(marked(fieldA), marked(fieldB), pattern(fieldA), pattern(fieldB)) + for (factAccess in facts) { + val expected = requirements.filter { + baseOnlySummaryInitialMatches(factAccess, (it as BaseOnlyInitialFactAp).access) + }.toSet() + val actual = mutableListOf() + storage.filterTo(actual, final(factAccess, AccessPathBase.This)) + assertEquals(expected, actual.toSet(), "scan reference for ${manager.renderAccess(factAccess)}") + } + } + + @Test + fun `subscription filtering covers the corresponding Tree scenario`() { + val treeManager = TreeApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + RefManager(), + Cancellation(), + ) + val treeSub = treeManager.accessPathSubscription() + val baseOnlySub = manager.accessPathSubscription() + + val treeCallerInitial = treeManager.abstractInitialOf(AccessPathBase.Argument(0), fieldA) + val baseOnlyCallerInitial = manager.abstractInitialOf(AccessPathBase.Argument(0), fieldA) + treeSub.addFactToFact( + inst, + AccessPathBase.This, + treeCallerInitial, + treeManager.finalOf(AccessPathBase.Return, fieldA, mark), + ) + treeSub.addFactToFact( + inst, + AccessPathBase.This, + treeCallerInitial, + treeManager.finalOf(AccessPathBase.Return, fieldB, mark), + ) + baseOnlySub.addFactToFact( + inst, + AccessPathBase.This, + baseOnlyCallerInitial, + manager.finalOf(AccessPathBase.Return, fieldA, mark), + ) + baseOnlySub.addFactToFact( + inst, + AccessPathBase.This, + baseOnlyCallerInitial, + manager.finalOf(AccessPathBase.Return, fieldB, mark), + ) + + val treeResult = mutableListOf() + treeSub.collectFactEdge( + treeResult, + treeManager.abstractInitialOf(AccessPathBase.This, fieldA), + emptyDeltaRequired = false, + ) + val baseOnlyResult = mutableListOf() + baseOnlySub.collectFactEdge( + baseOnlyResult, + manager.abstractInitialOf(AccessPathBase.This, fieldA), + emptyDeltaRequired = false, + ) + + assertEquals(1, treeResult.size, "Tree scenario must select only field A") + assertTrue(baseOnlyResult.size >= treeResult.size, "BaseOnly dropped a Tree subscription match") + } + + private fun ApManager.abstractInitialOf(base: AccessPathBase, vararg accessors: Accessor): InitialFactAp { + var fact = mostAbstractInitialAp(base) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun ApManager.finalOf(base: AccessPathBase, vararg accessors: Accessor): FinalFactAp { + var fact = createFinalAp(base, ExclusionSet.Universe) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryNormalizationTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryNormalizationTest.kt new file mode 100644 index 000000000..b2aac7303 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryNormalizationTest.kt @@ -0,0 +1,150 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Edge +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactToFactEdgeBuilder +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseOnlySummaryNormalizationTest { + @Test + fun `field initial is moved to suffix when summary final has suffix`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val static = manager.interner.index(ClassStaticAccessor("S")) + val field = manager.interner.index(FieldAccessor("C", "f", "T")) + val initial = packBaseOnlyAccess(static, ABSTRACT_MARK, NO_ACCESSOR) + val final = packBaseOnlyAccess(static, field, ABSTRACT_MARK) + + val normalized = normalizeSummaryInitialAccess(initial, final) + + assertEquals(packBaseOnlyAccess(static, NO_ACCESSOR, ABSTRACT_MARK), normalized) + } + + @Test + fun `field initial is unchanged when summary final has field abstraction`() { + val initial = packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR) + val final = packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR) + + assertEquals(initial, normalizeSummaryInitialAccess(initial, final)) + } + + @Test + fun `suffix initial is unchanged`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val field = manager.interner.index(FieldAccessor("C", "f", "T")) + val initial = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK) + val final = packBaseOnlyAccess(NO_ACCESSOR, field, ABSTRACT_MARK) + + assertEquals(initial, normalizeSummaryInitialAccess(initial, final)) + } + + @Test + fun `normalized aliases are queryable but do not report deltas`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager) + val static = manager.interner.index(ClassStaticAccessor("S")) + val field = manager.interner.index(FieldAccessor("C", "f", "T")) + val initialAccess = packBaseOnlyAccess(static, ABSTRACT_MARK, NO_ACCESSOR) + val normalizedAccess = packBaseOnlyAccess(static, NO_ACCESSOR, ABSTRACT_MARK) + val finalAccess = packBaseOnlyAccess(static, field, ABSTRACT_MARK) + val edge = Edge.FactToFact( + entryPoint, + BaseOnlyInitialFactAp(manager, AccessPathBase.Argument(0), initialAccess, ExclusionSet.Empty), + inst, + BaseOnlyFinalFactAp(manager, AccessPathBase.Return, finalAccess, ExclusionSet.Empty), + ) + + val added = mutableListOf() + storage.add(listOf(edge), added) + + assertEquals(1, added.size, "the normalized alias must not be reported as a new summary delta") + assertEquals(initialAccess, added.single().buildForTest().initialAccess) + assertFalse(normalizedAccess in storage.initialAccesses(), "normalized aliases stay hidden until trace resolution") + + manager.enableTraceResolutionMode() + + val queried = storage.initialAccesses() + assertTrue(initialAccess in queried, "the original summary remains queryable") + assertTrue(normalizedAccess in queried, "the normalized alias remains available to trace resolution") + } + + @Test + fun `normalized aliases do not duplicate an exact primary summary`() { + val manager = BaseOnlyApManager(AnyAccessorUnrollStrategy.AnyAccessorDisabled, Cancellation()) + val storage = MethodInitialToFinalBaseOnlyApSummariesStorage(inst, manager) + val static = manager.interner.index(ClassStaticAccessor("S")) + val field = manager.interner.index(FieldAccessor("C", "f", "T")) + val originalInitial = packBaseOnlyAccess(static, ABSTRACT_MARK, NO_ACCESSOR) + val normalizedInitial = packBaseOnlyAccess(static, NO_ACCESSOR, ABSTRACT_MARK) + val finalAccess = packBaseOnlyAccess(static, field, ABSTRACT_MARK) + fun edge(initial: BaseOnlyAccess) = Edge.FactToFact( + entryPoint, + BaseOnlyInitialFactAp(manager, AccessPathBase.Argument(0), initial, ExclusionSet.Empty), + inst, + BaseOnlyFinalFactAp(manager, AccessPathBase.Return, finalAccess, ExclusionSet.Empty), + ) + + storage.add(listOf(edge(originalInitial), edge(normalizedInitial)), mutableListOf()) + manager.enableTraceResolutionMode() + + val result = mutableListOf() + storage.filterEdgesTo(result, initialFactPattern = null, finalFactBase = AccessPathBase.Return) + assertEquals(2, result.size, "the normalized alias duplicates the second primary edge exactly") + } + + private fun MethodInitialToFinalBaseOnlyApSummariesStorage.initialAccesses(): Set { + val result = mutableListOf() + filterEdgesTo(result, initialFactPattern = null, finalFactBase = AccessPathBase.Return) + return result.mapTo(hashSetOf()) { it.buildForTest().initialAccess } + } + + private fun FactToFactEdgeBuilder.buildForTest(): BuiltEdge = + setEntryPoint(entryPoint).build().let { + BuiltEdge((it.initialFactAp as BaseOnlyInitialFactAp).access) + } + + private data class BuiltEdge(val initialAccess: BaseOnlyAccess) + + private val method: CommonMethod = object : CommonMethod { + override val name: String = "summaryNormalization" + override val parameters: List = listOf(object : CommonMethodParameter { + override val type: CommonTypeName = object : CommonTypeName { + override val typeName: String = "java.lang.Object" + } + }) + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "java.lang.Object" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + + private val inst: CommonInst = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod get() = this@BaseOnlySummaryNormalizationTest.method + } + } + + private val entryPoint = MethodEntryPoint(EmptyMethodContext, inst) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTestUtils.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTestUtils.kt new file mode 100644 index 000000000..33643e08b --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTestUtils.kt @@ -0,0 +1,39 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX + +/** Test-only reference join for differential and relation assertions. */ +internal fun canonicalJoin(left: BaseOnlyAccess, right: BaseOnlyAccess): Set { + BaseOnlyAccessOps.requireCanonical(left) + BaseOnlyAccessOps.requireCanonical(right) + if (left == right || BaseOnlyAccessOps.covers(left, right)) return setOf(left) + if (BaseOnlyAccessOps.covers(right, left)) return setOf(right) + + if (left.staticIdx == right.staticIdx && + left.fieldIdx == right.fieldIdx && + left.suffixIdx == right.suffixIdx && + left.hasSemanticMark && + left.valueAccessorState != right.valueAccessorState + ) return setOf(left, right) + + if (left.staticIdx != right.staticIdx || + left.staticIdx == ABSTRACT_MARK || right.staticIdx == ABSTRACT_MARK + ) return setOf(ABSTRACT_EMPTY_ACCESS) + + val staticIdx = left.staticIdx + if (left.fieldIdx == ABSTRACT_MARK || right.fieldIdx == ABSTRACT_MARK) { + return setOf(packBaseOnlyAccess(staticIdx, ABSTRACT_MARK, NO_ACCESSOR)) + } + + val fieldIdx = if (left.fieldIdx == right.fieldIdx) left.fieldIdx else NO_ACCESSOR + val suffixIdx = if (left.suffixIdx == right.suffixIdx) left.suffixIdx else ABSTRACT_MARK + if (suffixIdx >= 0 && suffixIdx != FINAL_ACCESSOR_IDX && + left.valueAccessorState != right.valueAccessorState + ) { + return setOf( + packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, left.valueAccessorState), + packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, right.valueAccessorState), + ) + } + return setOf(packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx)) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTracePremiseSubsumptionLawTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTracePremiseSubsumptionLawTest.kt new file mode 100644 index 000000000..90508e975 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTracePremiseSubsumptionLawTest.kt @@ -0,0 +1,90 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdge +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdges +import org.opentaint.dataflow.util.Cancellation +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class BaseOnlyTracePremiseSubsumptionLawTest { + @Test + fun `abstract and mark-specific initial premises are distinct and do not subsume each other`() { + val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + val base = AccessPathBase.Argument(0) + val abstractPremise = manager.mostAbstractInitialAp(base) + .replaceExclusions(ExclusionSet.Universe) + val markPremise = manager.createFinalInitialAp(base, ExclusionSet.Universe) + .prependAccessor(TaintMarkAccessor("trace-premise-cartesian")) + val abstractIncoming = manager.mostAbstractFinalAp(base) + .replaceExclusions(ExclusionSet.Universe) + val markIncoming = manager.createFinalAp(base, ExclusionSet.Universe) + .prependAccessor(TaintMarkAccessor("trace-premise-cartesian")) + + assertNotEquals(abstractPremise, markPremise) + assertFalse(abstractPremise.contains(markPremise)) + assertFalse(markPremise.contains(abstractPremise)) + assertTrue(abstractIncoming.equalTo(abstractPremise)) + assertTrue(abstractIncoming.contains(abstractPremise)) + assertFalse( + abstractIncoming.contains(markPremise), + "$abstractIncoming satisfies $abstractPremise but not the stronger $markPremise", + ) + assertFalse(markIncoming.contains(abstractPremise)) + assertTrue(markIncoming.equalTo(markPremise)) + assertTrue(markIncoming.contains(markPremise)) + } + + @Test + fun `collapsing conjunctive conclusions distributes premises instead of OR merging them`() { + val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + fun fact(base: AccessPathBase, mark: String) = manager + .createFinalInitialAp(base, ExclusionSet.Universe) + .prependAccessor(TaintMarkAccessor(mark)) + + val leftConclusion = fact(AccessPathBase.Return, "left") + val rightConclusion = fact(AccessPathBase.Return, "right") + val target = fact(AccessPathBase.Return, "target") + val leftPremises = listOf( + fact(AccessPathBase.Argument(0), "a0"), + fact(AccessPathBase.Argument(1), "a1"), + ) + val rightPremises = listOf( + fact(AccessPathBase.Argument(2), "b0"), + fact(AccessPathBase.Argument(3), "b1"), + ) + val formula = TraceEdges.of( + leftPremises.map { TraceEdge.MethodTraceEdge(it, leftConclusion) } + + rightPremises.map { TraceEdge.MethodTraceEdge(it, rightConclusion) } + ) + + val collapsed = formula.collapseToFact(target) + val alternatives = collapsed.premisesByFinalFact.getValue(target) + + assertEquals(4, alternatives.size) + assertTrue(alternatives.all { it is TraceEdge.MethodTraceNDEdge }) + assertEquals( + setOf( + setOf(leftPremises[0], rightPremises[0]), + setOf(leftPremises[0], rightPremises[1]), + setOf(leftPremises[1], rightPremises[0]), + setOf(leftPremises[1], rightPremises[1]), + ), + alternatives.mapTo(hashSetOf()) { (it as TraceEdge.MethodTraceNDEdge).initialFacts }, + ) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialOperationsTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialOperationsTest.kt new file mode 100644 index 000000000..1570799bb --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialOperationsTest.kt @@ -0,0 +1,773 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.access.AccessorList +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.ApManager +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.ReadableAccessorList +import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.ap.ifds.serialization.ApSerializer +import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext +import org.opentaint.dataflow.util.RefManager +import org.opentaint.ir.api.common.CommonMethod +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Executable Tree conformance for BaseOnly operations. + * + * These tests deliberately compare observable path languages rather than packed representations: + * BaseOnly is allowed to widen a Tree result, but every sequence readable from Tree must remain + * readable from at least one corresponding BaseOnly result. + */ +class BaseOnlyTreeDifferentialOperationsTest { + private val base = AccessPathBase.Argument(0) + private val stat = ClassStaticAccessor("example.Owner") + private val field = FieldAccessor("example.Owner", "value", "example.Value") + private val otherField = FieldAccessor("example.Value", "next", "example.Result") + private val mark = TaintMarkAccessor("source") + private val typeInfo = TypeInfoAccessor("example.Owner#getValue") + + private val unrollStructural = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + AnyAccessor.containsAccessor(accessor) + } + + private fun managers(): Pair = + TreeApManager(unrollStructural, RefManager(), Cancellation()) to + BaseOnlyApManager(unrollStructural, Cancellation(), fieldSensitive = true) + + private fun ApManager.finalOf(vararg accessors: Accessor): FinalFactAp { + var fact = createFinalAp(base, ExclusionSet.Empty) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun ApManager.abstractInitialOf(vararg accessors: Accessor): InitialFactAp { + var fact = mostAbstractInitialAp(base) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun ApManager.abstractFinalOf(vararg accessors: Accessor): FinalFactAp { + var fact = mostAbstractFinalAp(base) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun ApManager.finalInitialOf(vararg accessors: Accessor): InitialFactAp { + var fact = createFinalInitialAp(base, ExclusionSet.Empty) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private val observations: List> by lazy { + val alphabet = listOf( + stat, + field, + otherField, + ElementAccessor, + AnyAccessor, + ValueAccessor, + mark, + TypeInfoGroupAccessor, + typeInfo, + FinalAccessor, + ) + buildList { + add(emptyList()) + var frontier = listOf(emptyList()) + repeat(4) { + frontier = frontier.flatMap { prefix -> alphabet.map { prefix + it } } + addAll(frontier) + } + } + } + + @Test + fun `BaseOnly empty-delta predicate is equivalent to materialized delta inspection`() { + val manager = managers().second + val finals = listOf( + manager.finalOf(mark), + manager.finalOf(field, mark), + manager.abstractFinalOf(field), + manager.mostAbstractFinalAp(base), + ) + val initials = listOf( + manager.finalInitialOf(mark), + manager.finalInitialOf(field, mark), + manager.abstractInitialOf(field), + manager.mostAbstractInitialAp(base), + ) + + for (finalFact in finals) { + for (initialFact in initials) { + assertEquals( + finalFact.delta(initialFact).any { it.isEmpty }, + finalFact.hasEmptyDelta(initialFact), + "final=$finalFact, initial=$initialFact", + ) + } + } + } + + private fun readable(list: ReadableAccessorList<*>, sequence: List): Boolean { + var current: ReadableAccessorList<*> = list + for (accessor in sequence) { + current = current.readAccessor(accessor) as? ReadableAccessorList<*> ?: return false + } + return true + } + + private fun assertOverapproximates( + treeResults: Collection>, + baseOnlyResults: Collection>, + scenario: String, + ) { + for (sequence in observations) { + if (treeResults.none { readable(it, sequence) }) continue + assertTrue( + baseOnlyResults.any { readable(it, sequence) }, + "$scenario lost readable sequence ${sequence.joinToString(" -> ")}", + ) + } + } + + private fun assertReadAndStartConformance( + tree: ReadableAccessorList<*>, + baseOnly: ReadableAccessorList<*>, + scenario: String, + ) { + val probes = listOf( + stat, + field, + otherField, + ElementAccessor, + AnyAccessor, + ValueAccessor, + mark, + typeInfo, + FinalAccessor, + ) + for (probe in probes) { + assertEquals( + baseOnly.readAccessor(probe) != null, + baseOnly.startsWithAccessor(probe), + "$scenario: BaseOnly read/startsWith disagree for $probe", + ) + if (tree.readAccessor(probe) != null) { + assertNotNull(baseOnly.readAccessor(probe), "$scenario lost Tree read for $probe") + } + if (tree.startsWithAccessor(probe)) { + assertTrue(baseOnly.startsWithAccessor(probe), "$scenario lost Tree start for $probe") + } + } + + for (treeStart in tree.getStartAccessors()) { + val represented = treeStart in baseOnly.getStartAccessors() || + (AnyAccessor.containsAccessor(treeStart) && AnyAccessor in baseOnly.getStartAccessors()) + assertTrue(represented, "$scenario lost symbolic Tree start edge $treeStart") + } + } + + @Test + fun `abstract status follows the current logical root after concrete prefix reads`() { + val (treeManager, baseOnlyManager) = managers() + + fun assertRootStatus( + tree: ReadableAccessorList<*>, + baseOnly: ReadableAccessorList<*>, + expected: Boolean, + stage: String, + ) { + assertEquals(expected, tree.isAbstract(), "$stage: unexpected Tree status") + assertEquals(tree.isAbstract(), baseOnly.isAbstract(), "$stage: BaseOnly differs from Tree") + } + + var treeFinal: ReadableAccessorList<*> = treeManager.abstractFinalOf(stat, field) + var baseOnlyFinal: ReadableAccessorList<*> = baseOnlyManager.abstractFinalOf(stat, field) + assertRootStatus(treeFinal, baseOnlyFinal, expected = false, "final before prefix reads") + treeFinal = assertNotNull(treeFinal.readAccessor(stat) as? ReadableAccessorList<*>) + baseOnlyFinal = assertNotNull(baseOnlyFinal.readAccessor(stat) as? ReadableAccessorList<*>) + assertRootStatus(treeFinal, baseOnlyFinal, expected = false, "final after static read") + treeFinal = assertNotNull(treeFinal.readAccessor(field) as? ReadableAccessorList<*>) + baseOnlyFinal = assertNotNull(baseOnlyFinal.readAccessor(field) as? ReadableAccessorList<*>) + assertRootStatus(treeFinal, baseOnlyFinal, expected = true, "final after complete prefix read") + + var treeInitial: ReadableAccessorList<*> = treeManager.abstractInitialOf(stat, field) + var baseOnlyInitial: ReadableAccessorList<*> = baseOnlyManager.abstractInitialOf(stat, field) + assertRootStatus(treeInitial, baseOnlyInitial, expected = false, "initial before prefix reads") + treeInitial = assertNotNull(treeInitial.readAccessor(stat) as? ReadableAccessorList<*>) + baseOnlyInitial = assertNotNull(baseOnlyInitial.readAccessor(stat) as? ReadableAccessorList<*>) + assertRootStatus(treeInitial, baseOnlyInitial, expected = false, "initial after static read") + treeInitial = assertNotNull(treeInitial.readAccessor(field) as? ReadableAccessorList<*>) + baseOnlyInitial = assertNotNull(baseOnlyInitial.readAccessor(field) as? ReadableAccessorList<*>) + assertRootStatus(treeInitial, baseOnlyInitial, expected = true, "initial after complete prefix read") + } + + @Test + fun `prepend composes with read startsWith and accessor views without losing Tree paths`() { + val (treeManager, baseOnlyManager) = managers() + var tree = treeManager.finalOf(AnyAccessor, mark) + var baseOnly = baseOnlyManager.finalOf(AnyAccessor, mark) + + fun verify(stage: String) { + assertOverapproximates(listOf(tree), listOf(baseOnly), stage) + assertReadAndStartConformance(tree, baseOnly, stage) + assertFalse(AnyAccessor in tree.getAllAccessors(), "$stage: Tree all-accessor view exposed Any") + assertFalse(AnyAccessor in baseOnly.getAllAccessors(), "$stage: BaseOnly all-accessor view exposed Any") + } + + verify("any-mark suffix") + assertTrue(AnyAccessor in tree.getStartAccessors()) + assertTrue(AnyAccessor in baseOnly.getStartAccessors()) + + tree = tree.prependAccessor(field) + baseOnly = baseOnly.prependAccessor(field) + verify("field prepend") + + tree = tree.prependAccessor(stat) + baseOnly = baseOnly.prependAccessor(stat) + verify("static prepend") + } + + @Test + fun `construction with two fields retains the outer field and covers the inner Tree path`() { + val (treeManager, baseOnlyManager) = managers() + val tree = treeManager.finalOf(field, otherField, mark) + val baseOnly = baseOnlyManager.finalOf(field, otherField, mark) + + assertOverapproximates(listOf(tree), listOf(baseOnly), "two-field construction") + assertTrue(baseOnly.startsWithAccessor(field)) + val afterOuter = assertNotNull(baseOnly.readAccessor(field)) + assertTrue(afterOuter.startsWithAccessor(otherField), "discarded inner field must be covered by Any") + } + + @Test + fun `Tree Any is a start edge but never an all-accessor value`() { + val (treeManager, baseOnlyManager) = managers() + val tree = treeManager.finalOf(AnyAccessor, mark) + val baseOnly = baseOnlyManager.finalOf(AnyAccessor, mark) + + assertEquals(setOf(AnyAccessor), tree.getStartAccessors()) + assertTrue(AnyAccessor in baseOnly.getStartAccessors()) + assertFalse(AnyAccessor in tree.getAllAccessors()) + assertFalse(AnyAccessor in baseOnly.getAllAccessors()) + assertTrue(mark in tree.getAllAccessors()) + assertTrue(mark in baseOnly.getAllAccessors()) + } + + @Test + fun `type-info logical views and reads overapproximate Tree`() { + val (treeManager, baseOnlyManager) = managers() + val tree = treeManager.finalOf(TypeInfoGroupAccessor, typeInfo) + val baseOnly = baseOnlyManager.finalOf(TypeInfoGroupAccessor, typeInfo) + + assertOverapproximates(listOf(tree), listOf(baseOnly), "type-info") + assertReadAndStartConformance(tree, baseOnly, "type-info") + assertTrue(TypeInfoGroupAccessor in baseOnly.getStartAccessors()) + assertTrue( + baseOnly.getAllAccessors().containsAll(tree.getAllAccessors()), + "BaseOnly logical all-accessor view lost ${tree.getAllAccessors() - baseOnly.getAllAccessors()}", + ) + assertEquals(baseOnly, baseOnly.clearAccessor(TypeInfoGroupAccessor)) + assertEquals(baseOnly, baseOnly.clearAccessor(typeInfo)) + } + + @Test + fun `build and prepend preserve Value then taint mark composite suffix`() { + val (treeManager, baseOnlyManager) = managers() + val tree = treeManager.finalOf(ValueAccessor, mark) + val baseOnly = baseOnlyManager.finalOf(ValueAccessor, mark) + val builtAccess = BaseOnlyAccessOps.build( + intArrayOf( + baseOnlyManager.interner.index(ValueAccessor), + baseOnlyManager.interner.index(mark), + baseOnlyManager.interner.index(FinalAccessor), + ), + isAbstract = false, + ) + val builtBaseOnly = BaseOnlyFinalFactAp(baseOnlyManager, base, builtAccess, ExclusionSet.Empty) + + assertOverapproximates(listOf(tree), listOf(baseOnly), "Value -> mark -> final") + assertOverapproximates(listOf(tree), listOf(builtBaseOnly), "build(Value -> mark -> final)") + assertReadAndStartConformance(tree, baseOnly, "Value -> mark -> final") + assertTrue(ValueAccessor in baseOnly.getStartAccessors()) + assertTrue(ValueAccessor in baseOnly.getAllAccessors()) + val afterValue = assertNotNull(baseOnly.readAccessor(ValueAccessor)) + assertTrue(afterValue.startsWithAccessor(mark), "reading Value must retain the following mark") + } + + @Test + fun `joining normal and value states retains two facts`() { + val (treeManager, baseOnlyManager) = managers() + val treeNormal = treeManager.finalOf(mark) as AccessTree + val treeValue = treeManager.finalOf(ValueAccessor, mark) as AccessTree + val treeUnion = AccessTree( + treeManager, + base, + treeNormal.access.mergeAdd(treeValue.access), + ExclusionSet.Empty, + ) + val normal = baseOnlyManager.finalOf(mark) as BaseOnlyFinalFactAp + val value = baseOnlyManager.finalOf(ValueAccessor, mark) as BaseOnlyFinalFactAp + val joined = canonicalJoin(normal.access, value.access).map { access -> + BaseOnlyFinalFactAp(baseOnlyManager, base, access, ExclusionSet.Empty) + } + + assertEquals( + setOf(BaseOnlyValueAccessorState.Normal, BaseOnlyValueAccessorState.Value), + joined.mapTo(hashSetOf()) { it.access.valueAccessorState }, + ) + assertOverapproximates(listOf(treeUnion), joined, "joined value-accessor states") + assertEquals( + setOf(AnyAccessor, ValueAccessor, mark), + joined.flatMapTo(hashSetOf()) { it.getStartAccessors() }, + ) + assertTrue(joined.flatMapTo(hashSetOf()) { it.getAllAccessors() }.containsAll(treeUnion.getAllAccessors())) + + val treeAfterValue = assertNotNull(treeUnion.readAccessor(ValueAccessor)) + val baseOnlyAfterValue = joined.mapNotNull { it.readAccessor(ValueAccessor) } + assertOverapproximates(listOf(treeAfterValue), baseOnlyAfterValue, "Value read ValueAccessor") + assertTrue( + baseOnlyAfterValue.filterIsInstance() + .all { it.access.valueAccessorState == BaseOnlyValueAccessorState.Normal }, + ) + + val treeNormalInitial = treeManager.finalInitialOf(mark) + val treeValueInitial = treeManager.finalInitialOf(ValueAccessor, mark) + val normalInitial = baseOnlyManager.finalInitialOf(mark) + val valueInitial = baseOnlyManager.finalInitialOf(ValueAccessor, mark) + assertTrue(treeUnion.contains(treeNormalInitial)) + assertTrue(treeUnion.contains(treeValueInitial)) + assertTrue(joined.any { it.contains(normalInitial) }) + assertTrue(joined.any { it.contains(valueInitial) }) + + val treeInitial = treeManager.mostAbstractInitialAp(base) + val baseOnlyInitial = baseOnlyManager.mostAbstractInitialAp(base) + val treeTarget = treeManager.mostAbstractFinalAp(base) + val baseOnlyTarget = baseOnlyManager.mostAbstractFinalAp(base) + val treeResults = treeUnion.delta(treeInitial).mapNotNull { treeTarget.concat(FactTypeChecker.Dummy, it) } + val baseOnlyResults = joined.flatMap { fact -> + fact.delta(baseOnlyInitial).mapNotNull { baseOnlyTarget.concat(FactTypeChecker.Dummy, it) } + } + assertOverapproximates(treeResults, baseOnlyResults, "two-state delta + concat") + assertEquals( + setOf(BaseOnlyValueAccessorState.Normal, BaseOnlyValueAccessorState.Value), + baseOnlyResults.filterIsInstance() + .mapTo(hashSetOf()) { it.access.valueAccessorState }, + ) + } + + @Test + fun `final delta then concat overapproximates the corresponding Tree scenario`() { + val (treeManager, baseOnlyManager) = managers() + val treeSource = treeManager.finalOf(field, AnyAccessor, mark) + val baseOnlySource = baseOnlyManager.finalOf(field, AnyAccessor, mark) + val treeInitial = treeManager.abstractInitialOf(field, AnyAccessor) + val baseOnlyInitial = baseOnlyManager.abstractInitialOf(field, AnyAccessor) + val treeTarget = treeManager.abstractFinalOf(field, AnyAccessor) + val baseOnlyTarget = baseOnlyManager.abstractFinalOf(field, AnyAccessor) + + val treeResults = treeSource.delta(treeInitial).mapNotNull { + treeTarget.concat(FactTypeChecker.Dummy, it) + } + val baseOnlyResults = baseOnlySource.delta(baseOnlyInitial).mapNotNull { + baseOnlyTarget.concat(FactTypeChecker.Dummy, it) + } + + assertTrue(treeResults.isNotEmpty(), "Tree scenario must exercise delta + concat") + assertTrue(baseOnlyResults.isNotEmpty(), "BaseOnly rejected a Tree-applicable delta + concat scenario") + assertOverapproximates(treeResults, baseOnlyResults, "delta + concat") + } + + @Test + fun `final concat widens an extra structural delta and covers Tree`() { + val (treeManager, baseOnlyManager) = managers() + val treeTarget = treeManager.abstractFinalOf(field) + val baseOnlyTarget = baseOnlyManager.abstractFinalOf(field) + + for (suffix in listOf(listOf(otherField), listOf(otherField, mark))) { + val treeDelta = treeManager.finalOf(*suffix.toTypedArray()) + .delta(treeManager.mostAbstractInitialAp(base)) + .single() + val baseOnlyDelta = BaseOnlyNodeFinalDelta( + baseOnlyManager, + (baseOnlyManager.finalOf(*suffix.toTypedArray()) as BaseOnlyFinalFactAp).access, + ) + + val treeResult = assertNotNull(treeTarget.concat(FactTypeChecker.Dummy, treeDelta)) + val baseOnlyResult = assertNotNull(baseOnlyTarget.concat(FactTypeChecker.Dummy, baseOnlyDelta)) + + assertEquals(setOf(field), baseOnlyResult.getStartAccessors()) + assertTrue(treeResult.startsWithAccessor(field)) + if (suffix.last() == mark) { + assertTrue(baseOnlyResult.startsWithAccessor(field)) + val afterOuter = assertNotNull(baseOnlyResult.readAccessor(field)) + val afterInner = assertNotNull(afterOuter.readAccessor(otherField)) + assertTrue(afterInner.startsWithAccessor(mark), "absorbing inner field must preserve terminal") + assertEquals(baseOnlyManager.finalOf(field, mark), baseOnlyResult) + } else { + assertFalse(baseOnlyResult.isAbstract(), "the abstraction is still behind the retained field") + assertTrue(assertNotNull(baseOnlyResult.readAccessor(field)).isAbstract()) + assertEquals(baseOnlyTarget, baseOnlyResult) + } + } + } + + @Test + fun `final concat follows Tree path-filter semantics`() { + val (treeManager, baseOnlyManager) = managers() + val treeTarget = treeManager.abstractFinalOf(field) + val baseOnlyTarget = baseOnlyManager.abstractFinalOf(field) + val treeDelta = treeManager.finalOf(mark) + .delta(treeManager.mostAbstractInitialAp(base)) + .single() + val baseOnlyDelta = BaseOnlyNodeFinalDelta( + baseOnlyManager, + (baseOnlyManager.finalOf(mark) as BaseOnlyFinalFactAp).access, + ) + + val acceptPathRejectCompatibility = object : FactTypeChecker { + override fun filterFactByLocalType(actualType: org.opentaint.ir.api.common.CommonType?, factAp: FinalFactAp): FinalFactAp? = factAp + override fun accessPathFilter(accessPath: List): FactTypeChecker.FactApFilter = + FactTypeChecker.AlwaysAcceptFilter + override fun accessPathCompatibilityFilter(accessPath: List): FactTypeChecker.FactCompatibilityFilter = + object : FactTypeChecker.FactCompatibilityFilter { + override fun check(accessor: Accessor): FactTypeChecker.CompatibilityFilterResult = + FactTypeChecker.CompatibilityFilterResult.NotCompatible + } + } + val acceptedTree = assertNotNull(treeTarget.concat(acceptPathRejectCompatibility, treeDelta)) + val acceptedBaseOnly = assertNotNull(baseOnlyTarget.concat(acceptPathRejectCompatibility, baseOnlyDelta)) + assertOverapproximates(listOf(acceptedTree), listOf(acceptedBaseOnly), "concat path filter acceptance") + + val rejectFinal = object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + if (accessor == FinalAccessor) FactTypeChecker.FilterResult.Reject + else FactTypeChecker.FilterResult.Accept + } + val statefulReject = object : FactTypeChecker { + override fun filterFactByLocalType(actualType: org.opentaint.ir.api.common.CommonType?, factAp: FinalFactAp): FinalFactAp? = factAp + override fun accessPathFilter(accessPath: List): FactTypeChecker.FactApFilter = + object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + if (accessor == mark) FactTypeChecker.FilterResult.FilterNext(rejectFinal) + else FactTypeChecker.FilterResult.Reject + } + override fun accessPathCompatibilityFilter(accessPath: List): FactTypeChecker.FactCompatibilityFilter = + FactTypeChecker.AlwaysCompatibleFilter + } + + assertNull(treeTarget.concat(statefulReject, treeDelta)) + assertNull(baseOnlyTarget.concat(statefulReject, baseOnlyDelta)) + } + + @Test + fun `initial split delta then concat overapproximates the corresponding Tree scenario`() { + val (treeManager, baseOnlyManager) = managers() + val treeCaller = treeManager.abstractInitialOf(field, AnyAccessor) + val baseOnlyCaller = baseOnlyManager.abstractInitialOf(field, AnyAccessor) + val treeSummaryFinal = treeManager.abstractFinalOf(field) + val baseOnlySummaryFinal = baseOnlyManager.abstractFinalOf(field) + + val treeResults = treeCaller.splitDelta(treeSummaryFinal).map { (matched, delta) -> + matched.concat(delta) + } + val baseOnlyResults = baseOnlyCaller.splitDelta(baseOnlySummaryFinal).map { (matched, delta) -> + matched.concat(delta) + } + + assertTrue(treeResults.isNotEmpty(), "Tree scenario must exercise splitDelta + concat") + assertTrue(baseOnlyResults.isNotEmpty(), "BaseOnly rejected a Tree-applicable splitDelta + concat scenario") + assertOverapproximates(treeResults, baseOnlyResults, "splitDelta + concat") + } + + @Test + fun `root suffix concat covers both plain and implicit Any Tree prefixes`() { + val (treeManager, baseOnlyManager) = managers() + val treeDelta = treeManager.finalOf(otherField, mark) + .delta(treeManager.mostAbstractInitialAp(base)) + .single() + val baseOnlyDelta = BaseOnlyNodeFinalDelta( + baseOnlyManager, + (baseOnlyManager.finalOf(otherField, mark) as BaseOnlyFinalFactAp).access, + ) + + val treeRoot = assertNotNull( + treeManager.mostAbstractFinalAp(base).concat(FactTypeChecker.Dummy, treeDelta), + ) + val treeAfterField = assertNotNull( + treeManager.abstractFinalOf(field).concat(FactTypeChecker.Dummy, treeDelta), + ) + val baseOnlyResult = assertNotNull( + baseOnlyManager.mostAbstractFinalAp(base).concat(FactTypeChecker.Dummy, baseOnlyDelta), + ) + + assertEquals(baseOnlyManager.finalOf(AnyAccessor, mark), baseOnlyResult) + assertOverapproximates( + listOf(treeRoot, treeAfterField), + listOf(baseOnlyResult), + "root suffix concat with implicit Any", + ) + } + + @Test + fun `contains and equalTo preserve every Tree-true relation`() { + val (treeManager, baseOnlyManager) = managers() + val treeFinal = treeManager.finalOf(field, mark) + val baseOnlyFinal = baseOnlyManager.finalOf(field, mark) + val treeExactInitial = treeManager.finalInitialOf(field, mark) + val baseOnlyExactInitial = baseOnlyManager.finalInitialOf(field, mark) + + assertTrue(treeFinal.contains(treeExactInitial)) + assertTrue(baseOnlyFinal.contains(baseOnlyExactInitial), "BaseOnly lost Tree final containment") + assertTrue(treeFinal.equalTo(treeExactInitial)) + assertTrue(baseOnlyFinal.equalTo(baseOnlyExactInitial), "BaseOnly lost Tree cross-kind equality") + + val treeAbstractFinal = treeManager.abstractFinalOf(field) + val baseOnlyAbstractFinal = baseOnlyManager.abstractFinalOf(field) + val treeAbstractInitial = treeManager.abstractInitialOf(field) + val baseOnlyAbstractInitial = baseOnlyManager.abstractInitialOf(field) + assertTrue(treeAbstractFinal.contains(treeAbstractInitial)) + assertTrue(baseOnlyAbstractFinal.contains(baseOnlyAbstractInitial)) + + val treeExactInitialCopy = treeManager.finalInitialOf(field, mark) + val baseOnlyExactInitialCopy = baseOnlyManager.finalInitialOf(field, mark) + assertTrue(treeExactInitial.contains(treeExactInitialCopy)) + assertTrue(baseOnlyExactInitial.contains(baseOnlyExactInitialCopy)) + assertTrue( + baseOnlyExactInitial.contains(baseOnlyExactInitialCopy.exclude(otherField)), + "projected initial containment erases path-local exclusions conservatively", + ) + } + + @Test + fun `clear never removes a Tree-readable path`() { + val (treeManager, baseOnlyManager) = managers() + val tree = treeManager.finalOf(AnyAccessor, mark) + val baseOnly = baseOnlyManager.finalOf(AnyAccessor, mark) + + for (accessor in listOf(field, otherField, ElementAccessor, mark)) { + val treeCleared = tree.clearAccessor(accessor) + val baseOnlyCleared = baseOnly.clearAccessor(accessor) + if (treeCleared != null) { + assertNotNull(baseOnlyCleared, "clear($accessor) removed a surviving Tree result") + assertOverapproximates(listOf(treeCleared), listOf(baseOnlyCleared), "clear($accessor)") + } + } + + val treeExact = treeManager.finalOf(field, mark) + val baseOnlyExact = baseOnlyManager.finalOf(field, mark) + assertNull(treeExact.readAccessor(AnyAccessor)) + assertNull( + baseOnlyExact.readAccessor(AnyAccessor), + "an Any query must not consume an exact concrete-field edge", + ) + val treeAfterAnyClear = assertNotNull(treeExact.clearAccessor(AnyAccessor)) + val baseOnlyAfterAnyClear = assertNotNull( + baseOnlyExact.clearAccessor(AnyAccessor), + "clearing an Any edge must not clear an exact concrete-field edge", + ) + assertOverapproximates(listOf(treeAfterAnyClear), listOf(baseOnlyAfterAnyClear), "clear Any on exact field") + } + + @Test + fun `explicit Any projects to the implicit structural branch`() { + for (fieldSensitive in listOf(false, true)) { + val treeManager = TreeApManager(unrollStructural, RefManager(), Cancellation()) + val baseOnlyManager = BaseOnlyApManager( + unrollStructural, + Cancellation(), + fieldSensitive = fieldSensitive, + ) + val treeBare = treeManager.finalOf(mark) + val baseOnlyBare = baseOnlyManager.finalOf(mark) + + assertNull(treeBare.clearAccessor(mark)) + assertEquals(baseOnlyBare, baseOnlyBare.clearAccessor(mark)) + assertEquals(baseOnlyBare, baseOnlyBare.clearAccessor(ValueAccessor)) + + val treeAny = treeManager.finalOf(AnyAccessor, mark) + val baseOnlyAny = baseOnlyManager.finalOf(AnyAccessor, mark) as BaseOnlyFinalFactAp + assertEquals(NO_ACCESSOR, baseOnlyAny.access.fieldIdx) + assertEquals(baseOnlyBare, baseOnlyAny) + assertEquals(setOf(AnyAccessor, mark), baseOnlyAny.getStartAccessors()) + assertTrue(treeAny.startsWithAccessor(mark), "Tree Any child exposes its semantic suffix") + assertTrue(baseOnlyAny.startsWithAccessor(mark), "BaseOnly Any must expose the same suffix") + + val treeAfterConcrete = assertNotNull(treeAny.readAccessor(field)) + val baseOnlyAfterConcrete = assertNotNull(baseOnlyAny.readAccessor(field)) + assertOverapproximates( + listOf(treeAfterConcrete), + listOf(baseOnlyAfterConcrete), + "read concrete through Any", + ) + assertNotNull(treeAny.clearAccessor(mark)) + assertNotNull(baseOnlyAny.clearAccessor(mark), "clear(mark) does not remove an Any root edge") + } + } + + @Test + fun `fact and compatibility filters preserve all surviving Tree branches`() { + val (treeManager, baseOnlyManager) = managers() + val treeField = treeManager.finalOf(field, mark) as AccessTree + val treeOther = treeManager.finalOf(otherField, mark) as AccessTree + val treeMerged = AccessTree( + treeManager, + base, + treeField.access.mergeAdd(treeOther.access), + ExclusionSet.Empty, + ) + val baseOnlyBranches = listOf( + baseOnlyManager.finalOf(field, mark), + baseOnlyManager.finalOf(otherField, mark), + ) + + val branchFilter = object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + if (accessor == otherField) FactTypeChecker.FilterResult.Reject + else FactTypeChecker.FilterResult.Accept + } + val treeFiltered = listOfNotNull(treeMerged.filterFact(branchFilter)) + val baseOnlyFiltered = baseOnlyBranches.mapNotNull { it.filterFact(branchFilter) } + assertTrue(treeFiltered.isNotEmpty()) + assertOverapproximates(treeFiltered, baseOnlyFiltered, "fact branch filter") + + val compatibilityFilter = object : FactTypeChecker.FactCompatibilityFilter { + override fun check(accessor: Accessor): FactTypeChecker.CompatibilityFilterResult = + if (accessor == otherField) FactTypeChecker.CompatibilityFilterResult.NotCompatible + else FactTypeChecker.CompatibilityFilterResult.Compatible + } + val treeCompatible = listOfNotNull(treeMerged.filterFact(compatibilityFilter)) + val baseOnlyCompatible = baseOnlyBranches.mapNotNull { it.filterFact(compatibilityFilter) } + assertTrue(treeCompatible.isNotEmpty()) + assertEquals(2, baseOnlyCompatible.size, "Tree compatibility filtering never checks wholly concrete paths") + assertOverapproximates(treeCompatible, baseOnlyCompatible, "compatibility branch filter") + + val treeAbstractOther = treeManager.mostAbstractFinalAp(base).prependAccessor(otherField) + val baseOnlyAbstractOther = baseOnlyManager.mostAbstractFinalAp(base).prependAccessor(otherField) + assertNull(treeAbstractOther.filterFact(compatibilityFilter)) + assertNull(baseOnlyAbstractOther.filterFact(compatibilityFilter)) + + val rejectOuterPrefix = object : FactTypeChecker.FactCompatibilityFilter { + override fun check(accessor: Accessor): FactTypeChecker.CompatibilityFilterResult = + if (accessor == stat) FactTypeChecker.CompatibilityFilterResult.NotCompatible + else FactTypeChecker.CompatibilityFilterResult.Compatible + } + val treeNestedAbstract = treeManager.abstractFinalOf(stat, field) + val baseOnlyNestedAbstract = baseOnlyManager.abstractFinalOf(stat, field) + assertNotNull(treeNestedAbstract.filterFact(rejectOuterPrefix)) + assertNotNull(baseOnlyNestedAbstract.filterFact(rejectOuterPrefix)) + } + + @Test + fun `abstractOnly then rebase never removes a Tree-readable path`() { + val (treeManager, baseOnlyManager) = managers() + val tree = treeManager.finalOf(AnyAccessor, mark) + val baseOnly = baseOnlyManager.finalOf(AnyAccessor, mark) + val treeAbstract = tree.abstractOnly() + val baseOnlyAbstract = baseOnly.abstractOnly() + assertOverapproximates(listOf(treeAbstract), listOf(baseOnlyAbstract), "abstractOnly") + assertReadAndStartConformance(treeAbstract, baseOnlyAbstract, "abstractOnly") + + val newBase = AccessPathBase.Return + val treeRebased = treeAbstract.rebase(newBase) + val baseOnlyRebased = baseOnlyAbstract.rebase(newBase) + assertEquals(newBase, treeRebased.base) + assertEquals(newBase, baseOnlyRebased.base) + assertOverapproximates(listOf(treeRebased), listOf(baseOnlyRebased), "abstract rebase") + } + + @Test + fun `serialization preserves each domain and BaseOnly still overapproximates Tree`() { + val (treeManager, baseOnlyManager) = managers() + val context = InMemoryContext() + val treeSerializer = treeManager.createSerializer(context) + val baseOnlySerializer = baseOnlyManager.createSerializer(context) + val scenarios = listOf( + listOf(stat, field, AnyAccessor, mark), + listOf(TypeInfoGroupAccessor, typeInfo), + ) + + for (path in scenarios) { + val tree = treeManager.finalOf(*path.toTypedArray()) + val baseOnly = baseOnlyManager.finalOf(*path.toTypedArray()) + val restoredTree = roundTripFinal(treeSerializer, tree) + val restoredBaseOnly = roundTripFinal(baseOnlySerializer, baseOnly) + + assertEquals(tree, restoredTree, "Tree serialization changed $path") + assertEquals(baseOnly, restoredBaseOnly, "BaseOnly serialization changed $path") + assertOverapproximates(listOf(restoredTree), listOf(restoredBaseOnly), "serialization $path") + } + + val treeInitial = treeManager.finalInitialOf(field, mark) + val baseOnlyInitial = baseOnlyManager.finalInitialOf(field, mark) + val restoredTreeInitial = roundTripInitial(treeSerializer, treeInitial) + val restoredBaseOnlyInitial = roundTripInitial(baseOnlySerializer, baseOnlyInitial) + assertEquals(treeInitial, restoredTreeInitial) + assertEquals(baseOnlyInitial, restoredBaseOnlyInitial) + assertOverapproximates( + listOf(restoredTreeInitial), + listOf(restoredBaseOnlyInitial), + "initial serialization", + ) + } + + private fun roundTripFinal(serializer: ApSerializer, fact: FinalFactAp): FinalFactAp { + val bytes = ByteArrayOutputStream() + DataOutputStream(bytes).use { output -> with(serializer) { output.writeFinalAp(fact) } } + return DataInputStream(ByteArrayInputStream(bytes.toByteArray())).use { input -> + with(serializer) { input.readFinalAp() } + } + } + + private fun roundTripInitial(serializer: ApSerializer, fact: InitialFactAp): InitialFactAp { + val bytes = ByteArrayOutputStream() + DataOutputStream(bytes).use { output -> with(serializer) { output.writeInitialAp(fact) } } + return DataInputStream(ByteArrayInputStream(bytes.toByteArray())).use { input -> + with(serializer) { input.readInitialAp() } + } + } + + private class InMemoryContext : SummarySerializationContext { + private val accessorToId = HashMap() + private val idToAccessor = HashMap() + + override fun getIdByAccessor(accessor: Accessor): Long = + accessorToId.getOrPut(accessor) { + accessorToId.size.toLong().also { idToAccessor[it] = accessor } + } + + override fun getAccessorById(id: Long): Accessor = idToAccessor.getValue(id) + override fun getIdByMethod(method: CommonMethod): Long = error("not used") + override fun getMethodById(id: Long): CommonMethod = error("not used") + override fun loadSummaries(method: CommonMethod): ByteArray? = error("not used") + override fun storeSummaries(method: CommonMethod, summaries: ByteArray) = error("not used") + override fun flush() = error("not used") + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialStorageTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialStorageTest.kt new file mode 100644 index 000000000..3cfcb42a8 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialStorageTest.kt @@ -0,0 +1,561 @@ +package org.opentaint.dataflow.ap.ifds.access.baseonly + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor +import org.opentaint.dataflow.ap.ifds.Edge +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.LanguageManager +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils +import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication +import org.opentaint.dataflow.ap.ifds.SideEffectKind +import org.opentaint.dataflow.ap.ifds.SideEffectSummary.FactSideEffectSummary +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.ApManager +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.ReadableAccessorList +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.ap.ifds.serialization.MethodContextSerializer +import org.opentaint.dataflow.util.RefManager +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonCallExpr +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** Bounded, public-API differential scenarios for every BaseOnly storage family. */ +class BaseOnlyTreeDifferentialStorageTest { + private val fieldA = FieldAccessor("Owner", "a", "Value") + private val fieldB = FieldAccessor("Owner", "b", "Value") + private val mark = TaintMarkAccessor("source") + private val exA = ExclusionSet.Concrete(TaintMarkAccessor("excluded-a")) + private val exB = ExclusionSet.Concrete(TaintMarkAccessor("excluded-b")) + private val kind = object : SideEffectKind {} + private val entryPoint = MethodEntryPoint(EmptyMethodContext, inst) + + private fun managers(): Pair = + TreeApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + RefManager(), + Cancellation(), + ) to BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + summaryStorageFieldGeneralizationEnabled = true, + ) + + @Test + fun `intraprocedural Z2F F2F and ND sets cover Tree collection and deltas`() { + val (tree, baseOnly) = managers() + val treeZ2F = tree.methodEdgesFinalApSet(inst, 0, languageManager) + val baseOnlyZ2F = baseOnly.methodEdgesFinalApSet(inst, 0, languageManager) + val treeFinals = listOf(tree.finalOf(AccessPathBase.Return, fieldA, mark), tree.finalOf(AccessPathBase.Return, fieldB, mark)) + val baseOnlyFinals = listOf(baseOnly.finalOf(AccessPathBase.Return, fieldA, mark), baseOnly.finalOf(AccessPathBase.Return, fieldB, mark)) + + treeFinals.forEach { assertNotNull(treeZ2F.add(inst, it)) } + baseOnlyFinals.forEach { assertNotNull(baseOnlyZ2F.add(inst, it)) } + assertNull(treeZ2F.add(inst, treeFinals.first())) + assertNull(baseOnlyZ2F.add(inst, baseOnlyFinals.first())) + assertFinalCollectionCoversTree( + collectFinals { treeZ2F.collectApAtStatement(it, inst) }, + collectFinals { baseOnlyZ2F.collectApAtStatement(it, inst) }, + "intraprocedural Z2F collect-all", + ) + + val treeInitial = tree.initialOf(AccessPathBase.This, exA, fieldA) + val baseOnlyInitial = baseOnly.initialOf(AccessPathBase.This, exA, fieldA) + val treeF2F = tree.methodEdgesInitialToFinalApSet(inst, 0, languageManager) + val baseOnlyF2F = baseOnly.methodEdgesInitialToFinalApSet(inst, 0, languageManager) + treeFinals.forEach { assertTrue(treeF2F.add(inst, treeInitial, it.replaceExclusions(exA)).isNotEmpty()) } + baseOnlyFinals.forEach { assertTrue(baseOnlyF2F.add(inst, baseOnlyInitial, it.replaceExclusions(exA)).isNotEmpty()) } + assertTrue(treeF2F.add(inst, treeInitial, treeFinals.first().replaceExclusions(exA)).isEmpty()) + assertTrue(baseOnlyF2F.add(inst, baseOnlyInitial, baseOnlyFinals.first().replaceExclusions(exA)).isEmpty()) + + val treeF2FAll = mutableListOf>() + val baseOnlyF2FAll = mutableListOf>() + treeF2F.collectApAtStatement(treeF2FAll, inst) + baseOnlyF2F.collectApAtStatement(baseOnlyF2FAll, inst) + assertFinalCollectionCoversTree(treeF2FAll.map { it.second }, baseOnlyF2FAll.map { it.second }, "intraprocedural F2F collect-all") + assertTrue(baseOnlyF2FAll.all { it.second.exclusions == exA }) + + val treeF2FExact = mutableListOf() + val baseOnlyF2FExact = mutableListOf() + treeF2F.collectApAtStatement(treeF2FExact, inst, treeInitial, tree.mostAbstractInitialAp(AccessPathBase.Return)) + baseOnlyF2F.collectApAtStatement(baseOnlyF2FExact, inst, baseOnlyInitial, baseOnly.mostAbstractInitialAp(AccessPathBase.Return)) + assertFinalCollectionCoversTree(treeF2FExact, baseOnlyF2FExact, "intraprocedural F2F exact-initial") + + val treeNdInitial = setOf( + tree.initialOf(AccessPathBase.This, ExclusionSet.Universe, fieldA), + tree.initialOf(AccessPathBase.Exception, ExclusionSet.Universe, fieldB), + ) + val baseOnlyNdInitial = setOf( + baseOnly.initialOf(AccessPathBase.This, ExclusionSet.Universe, fieldA), + baseOnly.initialOf(AccessPathBase.Exception, ExclusionSet.Universe, fieldB), + ) + val treeND = tree.methodEdgesNDInitialToFinalApSet(inst, 0, languageManager) + val baseOnlyND = baseOnly.methodEdgesNDInitialToFinalApSet(inst, 0, languageManager) + treeFinals.forEach { assertNotNull(treeND.add(inst, treeNdInitial, it.replaceExclusions(ExclusionSet.Universe))) } + baseOnlyFinals.forEach { assertNotNull(baseOnlyND.add(inst, baseOnlyNdInitial, it.replaceExclusions(ExclusionSet.Universe))) } + assertNull(treeND.add(inst, treeNdInitial, treeFinals.first().replaceExclusions(ExclusionSet.Universe))) + assertNull(baseOnlyND.add(inst, baseOnlyNdInitial, baseOnlyFinals.first().replaceExclusions(ExclusionSet.Universe))) + + val treeNDAll = mutableListOf, FinalFactAp>>() + val baseOnlyNDAll = mutableListOf, FinalFactAp>>() + treeND.collectApAtStatement(treeNDAll, inst) + baseOnlyND.collectApAtStatement(baseOnlyNDAll, inst) + assertFinalCollectionCoversTree(treeNDAll.map { it.second }, baseOnlyNDAll.map { it.second }, "intraprocedural ND collect-all") + val treeNDExact = mutableListOf() + val baseOnlyNDExact = mutableListOf() + treeND.collectApAtStatement(treeNDExact, inst, treeNdInitial, tree.mostAbstractInitialAp(AccessPathBase.Return)) + baseOnlyND.collectApAtStatement(baseOnlyNDExact, inst, baseOnlyNdInitial, baseOnly.mostAbstractInitialAp(AccessPathBase.Return)) + assertFinalCollectionCoversTree(treeNDExact, baseOnlyNDExact, "intraprocedural ND exact-initial") + } + + @Test + fun `method Z2F F2F and ND summary queries cover Tree`() { + val (tree, baseOnly) = managers() + val treeFinals = listOf(tree.finalOf(AccessPathBase.Return, fieldA, mark), tree.finalOf(AccessPathBase.Return, fieldB, mark)) + val baseOnlyFinals = listOf(baseOnly.finalOf(AccessPathBase.Return, fieldA, mark), baseOnly.finalOf(AccessPathBase.Return, fieldB, mark)) + + val treeZ2F = tree.methodFinalApSummariesStorage(inst) + val baseOnlyZ2F = baseOnly.methodFinalApSummariesStorage(inst) + val treeZeroEdges = treeFinals.map { Edge.ZeroToFact(entryPoint, inst, it.replaceExclusions(ExclusionSet.Universe)) } + val baseOnlyZeroEdges = baseOnlyFinals.map { Edge.ZeroToFact(entryPoint, inst, it.replaceExclusions(ExclusionSet.Universe)) } + treeZ2F.add(treeZeroEdges, mutableListOf()) + baseOnlyZ2F.add(baseOnlyZeroEdges, mutableListOf()) + val treeZeroBuilders = mutableListOf() + val baseOnlyZeroBuilders = mutableListOf() + treeZ2F.filterEdgesTo(treeZeroBuilders, AccessPathBase.Return) + baseOnlyZ2F.filterEdgesTo(baseOnlyZeroBuilders, AccessPathBase.Return) + assertFinalCollectionCoversTree( + treeZeroBuilders.map { it.setEntryPoint(entryPoint).setExitStatement(inst).build().factAp }, + baseOnlyZeroBuilders.map { it.setEntryPoint(entryPoint).setExitStatement(inst).build().factAp }, + "method Z2F", + ) + + val treeInitial = tree.initialOf(AccessPathBase.This, exA, fieldA) + val baseOnlyInitial = baseOnly.initialOf(AccessPathBase.This, exA, fieldA) + val treeF2F = tree.methodInitialToFinalApSummariesStorage(inst) + val baseOnlyF2F = baseOnly.methodInitialToFinalApSummariesStorage(inst) + treeF2F.add(treeFinals.map { Edge.FactToFact(entryPoint, treeInitial, inst, it.replaceExclusions(exA)) }, mutableListOf()) + baseOnlyF2F.add(baseOnlyFinals.map { Edge.FactToFact(entryPoint, baseOnlyInitial, inst, it.replaceExclusions(exA)) }, mutableListOf()) + val treeF2FBuilders = mutableListOf() + val baseOnlyF2FBuilders = mutableListOf() + treeF2F.filterEdgesTo(treeF2FBuilders, tree.finalOf(AccessPathBase.This, fieldA), AccessPathBase.Return) + baseOnlyF2F.filterEdgesTo(baseOnlyF2FBuilders, baseOnly.finalOf(AccessPathBase.This, fieldA), AccessPathBase.Return) + val treeF2FFacts = treeF2FBuilders.map { it.setEntryPoint(entryPoint).setExitStatement(inst).build().factAp } + val baseOnlyF2FFacts = baseOnlyF2FBuilders.map { it.setEntryPoint(entryPoint).setExitStatement(inst).build().factAp } + assertFinalCollectionCoversTree(treeF2FFacts, baseOnlyF2FFacts, "method F2F patterned") + + val treeNdInitial = setOf( + tree.initialOf(AccessPathBase.This, ExclusionSet.Universe, fieldA), + tree.initialOf(AccessPathBase.Exception, ExclusionSet.Universe, fieldB), + ) + val baseOnlyNdInitial = setOf( + baseOnly.initialOf(AccessPathBase.This, ExclusionSet.Universe, fieldA), + baseOnly.initialOf(AccessPathBase.Exception, ExclusionSet.Universe, fieldB), + ) + val treeND = tree.methodNDInitialToFinalApSummariesStorage(inst) + val baseOnlyND = baseOnly.methodNDInitialToFinalApSummariesStorage(inst) + treeND.add(treeFinals.map { Edge.NDFactToFact(entryPoint, treeNdInitial, inst, it.replaceExclusions(ExclusionSet.Universe)) }, mutableListOf()) + baseOnlyND.add(baseOnlyFinals.map { Edge.NDFactToFact(entryPoint, baseOnlyNdInitial, inst, it.replaceExclusions(ExclusionSet.Universe)) }, mutableListOf()) + val treeNDBuilders = mutableListOf() + val baseOnlyNDBuilders = mutableListOf() + treeND.filterEdgesTo(treeNDBuilders, tree.mostAbstractFinalAp(AccessPathBase.This), AccessPathBase.Return) + baseOnlyND.filterEdgesTo(baseOnlyNDBuilders, baseOnly.mostAbstractFinalAp(AccessPathBase.This), AccessPathBase.Return) + assertFinalCollectionCoversTree( + treeNDBuilders.map { it.setEntryPoint(entryPoint).setExitStatement(inst).build().factAp }, + baseOnlyNDBuilders.map { it.setEntryPoint(entryPoint).setExitStatement(inst).build().factAp }, + "method ND patterned", + ) + } + + @Test + fun `method ND summary query does not enumerate a different concrete static accessor`() { + val (_, baseOnly) = managers() + val staticA = ClassStaticAccessor("StaticA") + val staticB = ClassStaticAccessor("StaticB") + val storage = baseOnly.methodNDInitialToFinalApSummariesStorage(inst) + val initialA = setOf( + baseOnly.initialOf(AccessPathBase.ClassStatic, ExclusionSet.Universe, staticA, mark), + baseOnly.initialOf(AccessPathBase.This, ExclusionSet.Universe, fieldA), + ) + val initialB = setOf( + baseOnly.initialOf(AccessPathBase.ClassStatic, ExclusionSet.Universe, staticB, mark), + baseOnly.initialOf(AccessPathBase.Exception, ExclusionSet.Universe, fieldB), + ) + storage.add( + listOf( + Edge.NDFactToFact( + entryPoint, + initialA, + inst, + baseOnly.finalOf(AccessPathBase.Return, ExclusionSet.Universe, fieldA, mark), + ), + Edge.NDFactToFact( + entryPoint, + initialB, + inst, + baseOnly.finalOf(AccessPathBase.Return, ExclusionSet.Universe, fieldB, mark), + ), + ), + mutableListOf(), + ) + + val selected = mutableListOf() + storage.filterEdgesTo( + selected, + baseOnly.finalOf(AccessPathBase.ClassStatic, staticA, mark), + AccessPathBase.Return, + ) + + assertEquals(1, selected.size) + assertEquals( + initialA, + selected.single().setEntryPoint(entryPoint).setExitStatement(inst).build().initialFacts, + ) + } + + @Test + fun `generalized F2F summaries cover every Tree member application`() { + val (tree, baseOnly) = managers() + val fields = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + FieldAccessor("Owner", "generalized-$index", "Value") + } + val marks = fields.indices.map { index -> TaintMarkAccessor("generalized-mark-$index") } + val treeStorage = tree.methodInitialToFinalApSummariesStorage(inst) + val baseOnlyStorage = baseOnly.methodInitialToFinalApSummariesStorage(inst) + + treeStorage.add( + fields.mapIndexed { index, field -> + Edge.FactToFact( + entryPoint, + tree.initialOf( + AccessPathBase.This, + if (index == 0) exA else ExclusionSet.Empty, + field, + ), + inst, + tree.abstractFinalOf(AccessPathBase.Return, fields.reversed()[index]) + .replaceExclusions(if (index == 0) exA else ExclusionSet.Empty), + ) + }, + mutableListOf(), + ) + baseOnlyStorage.add( + fields.mapIndexed { index, field -> + Edge.FactToFact( + entryPoint, + baseOnly.initialOf( + AccessPathBase.This, + if (index == 0) exA else ExclusionSet.Empty, + field, + ), + inst, + baseOnly.abstractFinalOf(AccessPathBase.Return, fields.reversed()[index]) + .replaceExclusions(if (index == 0) exA else ExclusionSet.Empty), + ) + }, + mutableListOf(), + ) + + val baseOnlyStored = mutableListOf() + baseOnlyStorage.filterEdgesTo( + baseOnlyStored, + initialFactPattern = null, + finalFactBase = AccessPathBase.Return, + ) + assertEquals(1, baseOnlyStored.size, "the eligible field family must be generalized") + + fields.forEachIndexed { index, field -> + val treeSelected = mutableListOf() + val baseOnlySelected = mutableListOf() + treeStorage.filterEdgesTo( + treeSelected, + tree.finalOf(AccessPathBase.This, field, marks[index]), + AccessPathBase.Return, + ) + baseOnlyStorage.filterEdgesTo( + baseOnlySelected, + baseOnly.finalOf(AccessPathBase.This, field, marks[index]), + AccessPathBase.Return, + ) + + assertTrue(treeSelected.isNotEmpty(), "Tree member $index must be applicable") + assertTrue(baseOnlySelected.isNotEmpty(), "generalization lost Tree member $index") + val input = baseOnly.finalOf(AccessPathBase.This, field, marks[index]) + val applied = baseOnlySelected.flatMap { builder -> + val summary = builder + .setEntryPoint(entryPoint) + .setExitStatement(inst) + .build() + MethodSummaryEdgeApplicationUtils.tryApplySummaryEdge( + input, + summary.initialFactAp, + ).mapNotNull { effect -> + when (effect) { + is SummaryEdgeApplication.SummaryApRefinement -> + summary.factAp + .concat(FactTypeChecker.Dummy, effect.delta) + ?.replaceExclusions(input.exclusions) + + is SummaryEdgeApplication.SummaryExclusionRefinement -> + summary.factAp.replaceExclusions(effect.exclusion) + } + } + } + val expected = baseOnly.exactInitialOf( + AccessPathBase.Return, + fields.reversed()[index], + marks[index], + ) + assertTrue( + applied.any { it.contains(expected) }, + "applying the generalized summary does not cover Tree member $index: $applied", + ) + } + } + + @Test + fun `field generalization keeps concrete field mark mappings exact`() { + val (_, baseOnly) = managers() + val fields = (0..MAX_FIELD_ENUMERATION_EDGES).map { index -> + FieldAccessor("Owner", "marked-$index", "Value") + } + val storage = baseOnly.methodInitialToFinalApSummariesStorage(inst) + storage.add( + fields.mapIndexed { index, field -> + val memberMark = TaintMarkAccessor("member-$index") + Edge.FactToFact( + entryPoint, + baseOnly.exactInitialOf(AccessPathBase.This, field, memberMark), + inst, + baseOnly.finalOf(AccessPathBase.Return, fields.reversed()[index], memberMark), + ) + }, + mutableListOf(), + ) + + val stored = mutableListOf() + storage.filterEdgesTo(stored, initialFactPattern = null, finalFactBase = AccessPathBase.Return) + assertEquals(fields.size, stored.size) + } + + @Test + fun `fact side effects and requirements cover Tree filtering and exclusion union`() { + val (tree, baseOnly) = managers() + val treeInitialA = tree.initialOf(AccessPathBase.This, exA, fieldA) + val baseOnlyInitialA = baseOnly.initialOf(AccessPathBase.This, exA, fieldA) + val treeInitialB = tree.initialOf(AccessPathBase.This, exA, fieldB) + val baseOnlyInitialB = baseOnly.initialOf(AccessPathBase.This, exA, fieldB) + + val treeSE = tree.factSideEffectSummariesApStorage(inst) + val baseOnlySE = baseOnly.factSideEffectSummariesApStorage(inst) + treeSE.add(listOf(FactSideEffectSummary(treeInitialA, kind), FactSideEffectSummary(treeInitialB, kind)), mutableListOf()) + baseOnlySE.add(listOf(FactSideEffectSummary(baseOnlyInitialA, kind), FactSideEffectSummary(baseOnlyInitialB, kind)), mutableListOf()) + treeSE.add(listOf(FactSideEffectSummary(treeInitialA.replaceExclusions(exB), kind)), mutableListOf()) + baseOnlySE.add(listOf(FactSideEffectSummary(baseOnlyInitialA.replaceExclusions(exB), kind)), mutableListOf()) + + val treeFiltered = mutableListOf() + val baseOnlyFiltered = mutableListOf() + treeSE.filterTaintedTo(treeFiltered, tree.finalOf(AccessPathBase.This, fieldA)) + baseOnlySE.filterTaintedTo(baseOnlyFiltered, baseOnly.finalOf(AccessPathBase.This, fieldA)) + assertEquals(1, treeFiltered.size) + assertTrue(baseOnlyFiltered.size >= treeFiltered.size) + assertEquals(exA.union(exB), baseOnlyFiltered.single().initialFactAp.exclusions) + + val treeReq = tree.sideEffectRequirementApStorage() + val baseOnlyReq = baseOnly.sideEffectRequirementApStorage() + treeReq.add(listOf(treeInitialA, treeInitialB)) + baseOnlyReq.add(listOf(baseOnlyInitialA, baseOnlyInitialB)) + treeReq.add(listOf(treeInitialA.replaceExclusions(exB))) + baseOnlyReq.add(listOf(baseOnlyInitialA.replaceExclusions(exB))) + val treeReqFiltered = mutableListOf() + val baseOnlyReqFiltered = mutableListOf() + treeReq.filterTo(treeReqFiltered, tree.finalOf(AccessPathBase.This, fieldA)) + baseOnlyReq.filterTo(baseOnlyReqFiltered, baseOnly.finalOf(AccessPathBase.This, fieldA)) + assertEquals(1, treeReqFiltered.size) + assertTrue(baseOnlyReqFiltered.size >= treeReqFiltered.size) + assertEquals(exA.union(exB), baseOnlyReqFiltered.single().exclusions) + val treeAll = mutableListOf() + val baseOnlyAll = mutableListOf() + treeReq.collectAllRequirementsTo(treeAll) + baseOnlyReq.collectAllRequirementsTo(baseOnlyAll) + assertEquals(treeAll.size, baseOnlyAll.size) + } + + @Test + fun `Z2F F2F and ND subscriptions cover Tree residual modes`() { + val (tree, baseOnly) = managers() + val treeSub = tree.accessPathSubscription() + val baseOnlySub = baseOnly.accessPathSubscription() + val treeCallerInitial = tree.initialOf(AccessPathBase.Return, ExclusionSet.Empty, fieldB) + val baseOnlyCallerInitial = baseOnly.initialOf(AccessPathBase.Return, ExclusionSet.Empty, fieldB) + val treeNdInitial = setOf(treeCallerInitial, tree.initialOf(AccessPathBase.Exception, ExclusionSet.Empty, fieldA)) + val baseOnlyNdInitial = setOf(baseOnlyCallerInitial, baseOnly.initialOf(AccessPathBase.Exception, ExclusionSet.Empty, fieldA)) + val treeExit = tree.finalOf(AccessPathBase.Return, fieldA, mark) + val baseOnlyExit = baseOnly.finalOf(AccessPathBase.Return, fieldA, mark) + val treeExactExit = tree.abstractFinalOf(AccessPathBase.Return, fieldA) + val baseOnlyExactExit = baseOnly.abstractFinalOf(AccessPathBase.Return, fieldA) + + for (exit in listOf(treeExit, treeExactExit)) { + treeSub.addZeroToFact(inst, AccessPathBase.This, exit) + treeSub.addFactToFact(inst, AccessPathBase.This, treeCallerInitial, exit) + treeSub.addNDFactToFact(inst, AccessPathBase.This, treeNdInitial, exit) + } + for (exit in listOf(baseOnlyExit, baseOnlyExactExit)) { + baseOnlySub.addZeroToFact(inst, AccessPathBase.This, exit) + baseOnlySub.addFactToFact(inst, AccessPathBase.This, baseOnlyCallerInitial, exit) + baseOnlySub.addNDFactToFact(inst, AccessPathBase.This, baseOnlyNdInitial, exit) + } + val treePattern = tree.initialOf(AccessPathBase.This, ExclusionSet.Empty, fieldA) + val baseOnlyPattern = baseOnly.initialOf(AccessPathBase.This, ExclusionSet.Empty, fieldA) + + val treeZero = mutableListOf() + val baseOnlyZero = mutableListOf() + treeSub.collectZeroEdge(treeZero, treePattern) + baseOnlySub.collectZeroEdge(baseOnlyZero, baseOnlyPattern) + assertTrue(baseOnlyZero.size >= treeZero.size, "BaseOnly dropped a Tree Z2F subscription") + + for (empty in listOf(false, true)) { + val treeFact = mutableListOf() + val baseOnlyFact = mutableListOf() + treeSub.collectFactEdge(treeFact, treePattern, empty) + baseOnlySub.collectFactEdge(baseOnlyFact, baseOnlyPattern, empty) + assertTrue( + baseOnlyFact.size >= treeFact.size, + "BaseOnly candidate broadcast dropped a Tree F2F subscription for empty=$empty", + ) + + val treeNd = mutableListOf() + val baseOnlyNd = mutableListOf() + treeSub.collectFactNDEdge(treeNd, treePattern, empty) + baseOnlySub.collectFactNDEdge(baseOnlyNd, baseOnlyPattern, empty) + assertTrue( + baseOnlyNd.size >= treeNd.size, + "BaseOnly candidate broadcast dropped a Tree ND subscription for empty=$empty", + ) + } + } + + @Test + fun `final fact list has Tree-equivalent index and LIFO behavior`() { + val (tree, baseOnly) = managers() + val treeList = tree.finalFactList() + val baseOnlyList = baseOnly.finalFactList() + val treeFacts = listOf(tree.finalOf(AccessPathBase.This, fieldA), tree.finalOf(AccessPathBase.Return, fieldB, mark)) + val baseOnlyFacts = listOf(baseOnly.finalOf(AccessPathBase.This, fieldA), baseOnly.finalOf(AccessPathBase.Return, fieldB, mark)) + treeFacts.forEach(treeList::add) + baseOnlyFacts.forEach(baseOnlyList::add) + for (idx in treeFacts.indices) { + assertFinalCollectionCoversTree(listOf(treeList.get(idx)), listOf(baseOnlyList.get(idx)), "final-list get($idx)") + } + assertFinalCollectionCoversTree(listOf(treeList.removeLast()), listOf(baseOnlyList.removeLast()), "final-list removeLast") + } + + private fun ApManager.finalOf(base: AccessPathBase, vararg accessors: Accessor): FinalFactAp = + finalOf(base, ExclusionSet.Empty, *accessors) + + private fun ApManager.abstractFinalOf(base: AccessPathBase, vararg accessors: Accessor): FinalFactAp { + var fact = mostAbstractFinalAp(base) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun ApManager.finalOf(base: AccessPathBase, exclusions: ExclusionSet, vararg accessors: Accessor): FinalFactAp { + var fact = createFinalAp(base, exclusions) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun ApManager.initialOf(base: AccessPathBase, exclusions: ExclusionSet, vararg accessors: Accessor): InitialFactAp { + var fact = mostAbstractInitialAp(base).replaceExclusions(exclusions) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun ApManager.exactInitialOf(base: AccessPathBase, vararg accessors: Accessor): InitialFactAp { + var fact = createFinalInitialAp(base, ExclusionSet.Empty) + accessors.reversed().forEach { fact = fact.prependAccessor(it) } + return fact + } + + private fun collectFinals(block: (MutableList) -> Unit): List = + mutableListOf().also(block) + + private fun readable(fact: ReadableAccessorList<*>, sequence: List): Boolean { + var current: ReadableAccessorList<*> = fact + for (accessor in sequence) { + current = current.readAccessor(accessor) as? ReadableAccessorList<*> ?: return false + } + return true + } + + private fun assertFinalCollectionCoversTree( + tree: Collection, + baseOnly: Collection, + scenario: String, + ) { + for (base in AccessPathBase.entriesForTest()) { + for (sequence in listOf(emptyList(), listOf(fieldA), listOf(fieldB), listOf(fieldA, mark), listOf(fieldB, mark))) { + if (tree.none { it.base == base && readable(it, sequence) }) continue + assertTrue( + baseOnly.any { it.base == base && readable(it, sequence) }, + "$scenario lost $base ${sequence.joinToString(" -> ")}", + ) + } + } + } + + private fun AccessPathBase.Companion.entriesForTest(): List = listOf( + AccessPathBase.This, + AccessPathBase.Return, + AccessPathBase.Argument(0), + AccessPathBase.Argument(1), + ) + + private val languageManager = object : LanguageManager { + override fun getInstIndex(inst: CommonInst): Int = 0 + override fun getMaxInstIndex(method: CommonMethod): Int = 0 + override fun getInstByIndex(method: CommonMethod, index: Int): CommonInst = Companion.inst + override fun isEmpty(method: CommonMethod): Boolean = false + override fun getCallExpr(inst: CommonInst): CommonCallExpr? = null + override fun producesExceptionalControlFlow(inst: CommonInst): Boolean = false + override fun getCalleeMethod(callExpr: CommonCallExpr): CommonMethod = error("unused") + override val methodContextSerializer: MethodContextSerializer get() = error("unused") + } + + private companion object { + val method = object : CommonMethod { + override val name: String = "storageDifferential" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { override val typeName: String = "void" } + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + val inst = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { override val method: CommonMethod = Companion.method } + override fun toString(): String = "storage-inst" + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/taint/ForwardActionableRulesRecorderTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/taint/ForwardActionableRulesRecorderTest.kt new file mode 100644 index 000000000..180e827ba --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/taint/ForwardActionableRulesRecorderTest.kt @@ -0,0 +1,54 @@ +package org.opentaint.dataflow.ap.ifds.taint + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotSame +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation + +class ForwardActionableRulesRecorderTest { + private val statement = object : CommonInst { + override val location: CommonInstLocation + get() = error("Unused by the recorder") + } + private val rule = object : CommonTaintConfigurationItem {} + private val action = object : CommonTaintAction {} + + @Test + fun `record is idempotent and snapshot is detached`() { + val recorder = ForwardActionableRulesRecorder() + + recorder.record(statement, rule, action) + recorder.record(statement, rule, action) + + val first = recorder.snapshot() + assertEquals(setOf(action), first.getValue(statement).getValue(rule)) + + recorder.clear() + val second = recorder.snapshot() + assertEquals(emptyMap(), second) + assertNotSame(first, second) + assertEquals(setOf(action), first.getValue(statement).getValue(rule)) + } + + @Test + fun `collect merges actions with an existing rule`() { + val recorder = ForwardActionableRulesRecorder() + val otherAction = object : CommonTaintAction {} + recorder.record(statement, rule, action) + + val collector: MutableMap< + CommonInst, + MutableMap>, + > = hashMapOf( + statement to hashMapOf( + rule to hashSetOf(otherAction), + ), + ) + recorder.collectInto(collector) + + assertEquals(setOf(action, otherAction), collector.getValue(statement).getValue(rule)) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/ExactProcessingTimeBudgetTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/ExactProcessingTimeBudgetTest.kt new file mode 100644 index 000000000..64d22827f --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/ExactProcessingTimeBudgetTest.kt @@ -0,0 +1,57 @@ +package org.opentaint.dataflow.ap.ifds.trace + +import org.opentaint.dataflow.util.Cancellation +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds + +class ExactProcessingTimeBudgetTest { + @Test + fun `budget stops an active operation and records its stage`() { + val budget = ExactProcessingTimeBudget(20.milliseconds) + + val measurement = budget.measure( + "vulnerability", + ExactProcessingTimeBudget.Stage.TRACE_RESOLUTION, + Cancellation(), + ) { operationCancellation -> + while (operationCancellation.isActive()) { + Thread.onSpinWait() + } + } + + assertTrue(measurement.snapshot.exhausted) + assertTrue(measurement.snapshot.traceResolution >= 20.milliseconds) + assertEquals(0.milliseconds, measurement.snapshot.ruleSearch) + } + + @Test + fun `trace and rule stages share one per-key budget`() { + val budget = ExactProcessingTimeBudget(30.milliseconds) + val parent = Cancellation() + + budget.measure( + "first", + ExactProcessingTimeBudget.Stage.TRACE_RESOLUTION, + parent, + ) { + Thread.sleep(10) + } + val measurement = budget.measure( + "first", + ExactProcessingTimeBudget.Stage.RULE_SEARCH, + parent, + ) { operationCancellation -> + while (operationCancellation.isActive()) { + Thread.onSpinWait() + } + } + + assertTrue(measurement.snapshot.exhausted) + assertTrue(measurement.snapshot.traceResolution >= 10.milliseconds) + assertTrue(measurement.snapshot.ruleSearch.isPositive()) + assertFalse(budget.isExhausted("second")) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/GraphReachabilityUtilTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/GraphReachabilityUtilTest.kt new file mode 100644 index 000000000..02c61f95a --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/GraphReachabilityUtilTest.kt @@ -0,0 +1,39 @@ +package org.opentaint.dataflow.ap.ifds.trace + +import kotlin.test.Test +import kotlin.test.assertEquals + +class GraphReachabilityUtilTest { + private data class Edge(val target: String, val enabled: Boolean) + + @Test + fun `reverse traversal finds every entry that can reach a target`() { + val graph = mapOf( + "root-a" to setOf(Edge("middle", enabled = true)), + "root-b" to setOf(Edge("dead", enabled = true)), + "middle" to setOf(Edge("target", enabled = true)), + "dead" to setOf(Edge("target", enabled = false)), + ) + + val reachable = entriesThatCanReach(graph, setOf("target")) { edge -> + edge.target.takeIf { edge.enabled } + } + + assertEquals(setOf("root-a", "middle", "target"), reachable) + } + + @Test + fun `reverse traversal supports multiple targets and cycles`() { + val graph = mapOf( + 1 to setOf(2), + 2 to setOf(1, 3), + 4 to setOf(5), + 6 to setOf(7), + ) + + assertEquals( + setOf(1, 2, 3, 4, 5), + entriesThatCanReach(graph, setOf(3, 5)) { it }, + ) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolverCacheTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolverCacheTest.kt new file mode 100644 index 000000000..dbcd6ed7e --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolverCacheTest.kt @@ -0,0 +1,63 @@ +package org.opentaint.dataflow.ap.ifds.trace + +import org.junit.jupiter.api.Test +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals + +class MethodTraceResolverCacheTest { + @Test + fun `concurrent resolvers compute a shared value once`() { + val cache = MethodTraceResolver.Cache() + val computations = AtomicInteger() + val start = CountDownLatch(1) + val executor = Executors.newFixedThreadPool(8) + + try { + val tasks = List(32) { + executor.submit> { + start.await() + cache.calleeEntryPoints(statement) { + computations.incrementAndGet() + emptyList() + } + } + } + start.countDown() + tasks.forEach { assertEquals(emptyList(), it.get(5, TimeUnit.SECONDS)) } + } finally { + executor.shutdownNow() + } + + assertEquals(1, computations.get()) + } + + private val statement = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod = object : CommonMethod { + override val name: String = "cache-test" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/SummaryTraceNormalizationTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/SummaryTraceNormalizationTest.kt new file mode 100644 index 000000000..f97cc11a5 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/SummaryTraceNormalizationTest.kt @@ -0,0 +1,95 @@ +package org.opentaint.dataflow.ap.ifds.trace + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.SummaryTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdge +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntry +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceKind +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals + +class SummaryTraceNormalizationTest { + @Test + fun `resolution identity ignores exclusions on every edge fact`() { + val first = summary(ExclusionSet.Empty) + val second = summary(ExclusionSet.Concrete(TaintMarkAccessor("excluded"))) + + assertEquals(first.withUniverseExclusions(), second.withUniverseExclusions()) + first.withUniverseExclusions().final.edges.forEach { edge -> + assertEquals(ExclusionSet.Universe, edge.fact.exclusions) + when (edge) { + is TraceEdge.MethodTraceEdge -> + assertEquals(ExclusionSet.Universe, edge.initialFact.exclusions) + + is TraceEdge.MethodTraceNDEdge -> edge.initialFacts.forEach { + assertEquals(ExclusionSet.Universe, it.exclusions) + } + + is TraceEdge.SourceTraceEdge -> Unit + } + } + } + + private fun summary(exclusions: ExclusionSet): SummaryTrace { + val first = fact(AccessPathBase.Argument(0), "first", exclusions) + val second = fact(AccessPathBase.Argument(1), "second", exclusions) + val final = fact(AccessPathBase.Return, "final", exclusions) + return SummaryTrace( + MethodEntryPoint(EmptyMethodContext, statement), + TraceEntry.Final( + setOf( + TraceEdge.SourceTraceEdge(final), + TraceEdge.MethodTraceEdge(first, final), + TraceEdge.MethodTraceNDEdge(setOf(first, second), final), + ), + statement, + ), + TraceKind.SummaryTrace, + ) + } + + private fun fact(base: AccessPathBase, mark: String, exclusions: ExclusionSet): InitialFactAp = + manager.mostAbstractInitialAp(base) + .prependAccessor(TaintMarkAccessor(mark)) + .replaceExclusions(exclusions) + + private val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + + private val statement = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod = object : CommonMethod { + override val name: String = "test" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/SharedMethodEntryBoundaryTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/SharedMethodEntryBoundaryTest.kt new file mode 100644 index 000000000..775e367d7 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/SharedMethodEntryBoundaryTest.kt @@ -0,0 +1,540 @@ +package org.opentaint.dataflow.ap.ifds.trace.action + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.FullStart2FinalTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.Start2FinalTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.SummaryTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdge +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntry +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntryAction +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceKind +import org.opentaint.dataflow.ap.ifds.trace.TraceResolver +import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.CallKind +import org.opentaint.dataflow.ap.ifds.trace.path.createSource2SinkGraph +import org.opentaint.dataflow.ap.ifds.trace.path.allMethodTraces +import org.opentaint.dataflow.ap.ifds.trace.path.methodGraph +import org.opentaint.dataflow.ap.ifds.trace.path.nodesForPathResolution +import org.opentaint.dataflow.ap.ifds.trace.path.processMethodTrace +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.CompactIntSet +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintAssignAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSink +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSinkMeta +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSource +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs + +class SharedMethodEntryBoundaryTest { + @Test + fun `shared boundary has linear edges and preserves rules on both sides`() { + val upstream = (0 until upstreamCount).map(::sourceNode) + val downstream = (0 until downstreamCount).map(::sinkNode) + val boundary = methodEntryBoundary() + val sourceToSink = sharedBoundaryTrace(upstream, boundary, downstream) + + val graph = createSource2SinkGraph(sourceToSink) + val boundaryId = graph.nodeIndices.getInt(boundary) + val upstreamIds = upstream.mapTo(linkedSetOf()) { graph.nodeIndices.getInt(it) } + val downstreamIds = downstream.mapTo(linkedSetOf()) { graph.nodeIndices.getInt(it) } + + assertEquals(upstreamCount + 1 + downstreamCount, graph.allNodes.size) + assertEquals(upstreamCount + downstreamCount, graph.root2SinkFwd.values.sumOf { it.size }) + upstreamIds.forEach { upstreamId -> + assertEquals(setOf(boundaryId), graph.root2SinkFwd.get(upstreamId).toSet()) + assertFalse( + downstreamIds.any { it in graph.root2SinkFwd.get(upstreamId) }, + "upstream nodes must not be copied once per downstream continuation", + ) + } + assertEquals(downstreamIds, graph.root2SinkFwd.get(boundaryId).toSet()) + + val materialized = linkedSetOf() + val downstreamRuleByNode = downstream.zip(downstreamRules).toMap() + val sinkStatement = downstream.first().trace.final.statement + val result = collectActionableRules( + trace = TraceResolver.Trace(entryPointToStart = null, sourceToSinkTrace = sourceToSink), + sinkStatement = sinkStatement, + sinkRules = setOf(sinkRule), + materializeNode = { node -> + materialized += node + val start2Final = node as TraceResolver.InterProceduralStart2FinalTraceNode + listOf(fullTrace(start2Final, downstreamRuleByNode[start2Final])) + }, + materializeSummary = { emptyList() }, + ) + + val collected = assertIs(result) + val expectedMaterialized: Set = + (upstream + downstream).toSet() + assertEquals(expectedMaterialized, materialized) + assertFalse(boundary in materialized) + sourceRules.forEachIndexed { index, rule -> + assertEquals( + setOf(sourceActions[index]), + collected.rules.getValue(upstream[index].trace.startEntry.statement).getValue(rule), + ) + } + downstreamRules.forEachIndexed { index, rule -> + assertEquals( + setOf(downstreamActions[index]), + collected.rules.getValue(downstream[index].trace.final.statement).getValue(rule), + ) + } + assertEquals(emptySet(), collected.rules.getValue(sinkStatement).getValue(sinkRule)) + } + + @Test + fun `shared boundary is transparent during path resolution`() { + val root = sourceNode(0) + val boundary = methodEntryBoundary() + val sink = sinkNode(0) + val graph = createSource2SinkGraph(sharedBoundaryTrace(listOf(root), boundary, listOf(sink))) + + val rootId = graph.nodeIndices.getInt(root) + val boundaryId = graph.nodeIndices.getInt(boundary) + val sinkId = graph.nodeIndices.getInt(sink) + val nodes = graph.nodesForPathResolution( + sink2Root = intArrayOf(sinkId, boundaryId, rootId), + root2Source = intArrayOf(rootId), + ) + + assertEquals(listOf(root), nodes.root2Source) + assertEquals(listOf(sink), nodes.root2SinkNoRoot) + } + + @Test + fun `same method boundary is transparent during node path reconstruction`() { + val root = sourceNode(0) + val boundary = methodEntryBoundary() + val sink = sinkNodeInMethod(0, boundary.entry.entryPoint) + val graph = createSource2SinkGraph(sharedBoundaryTrace(listOf(root), boundary, listOf(sink))) + val methodGraph = graph.methodGraph() + + val nodeTraces = methodGraph.allMethodTraces(limit = 1) { methodTrace -> + graph.processMethodTrace(methodGraph, methodTrace) { it } + } + + val nodeTrace = nodeTraces.single() + assertEquals( + listOf(sink, root), + nodeTrace.sink2Root.map { graph.allNodes[it] }, + ) + assertEquals( + listOf(root), + nodeTrace.root2Source.map { graph.allNodes[it] }, + ) + } + + @Test + fun `shared boundary preserves distinct action bearing summaries and transparent paths`() { + val root = sourceNode(0) + val boundary = methodEntryBoundary() + val sink = sinkNode(0) + val summaries = (0 until downstreamCount).map(::actionBearingSummary) + val summaryNodes = summaries.map { + TraceResolver.InterProceduralSummaryTraceNode(it.action.summaryTrace) + } + val summaryByTrace = summaries.associateBy { it.action.summaryTrace } + val sourceToSink = factoredSummaryTrace(root, boundary, summaryNodes, sink) + val materialized = linkedSetOf() + + val result = collectActionableRules( + trace = TraceResolver.Trace(entryPointToStart = null, sourceToSinkTrace = sourceToSink), + sinkStatement = sink.trace.final.statement, + sinkRules = setOf(sinkRule), + materializeNode = { node -> + materialized += node + when (node) { + is TraceResolver.InterProceduralStart2FinalTraceNode -> + listOf(fullTrace(node, downstreamRule = null)) + + is TraceResolver.InterProceduralSummaryTraceNode -> + listOf(summaryByTrace.getValue(node.trace).fullTrace) + + is TraceResolver.InterProceduralMethodEntryNode -> + error("synthetic boundary must not be materialized") + } + }, + materializeSummary = { error("no nested summary is expected") }, + ) + + val collected = assertIs(result) + assertEquals(setOf(root, sink) + summaryNodes, materialized) + assertFalse(boundary in materialized) + summaries.forEachIndexed { index, summary -> + val actionStatement = summary.fullTrace.entries[1].statement + assertEquals( + setOf(downstreamActions[index]), + collected.rules.getValue(actionStatement).getValue(downstreamRules[index]), + ) + } + + val graph = createSource2SinkGraph(sourceToSink) + val rootId = graph.nodeIndices.getInt(root) + val boundaryId = graph.nodeIndices.getInt(boundary) + val sinkId = graph.nodeIndices.getInt(sink) + val summaryIds = summaryNodes.mapTo(linkedSetOf()) { graph.nodeIndices.getInt(it) } + assertEquals(setOf(boundaryId), graph.root2SinkFwd.get(rootId).toSet()) + assertEquals(summaryIds, graph.root2SinkFwd.get(boundaryId).toSet()) + summaryNodes.forEach { summaryNode -> + val summaryId = graph.nodeIndices.getInt(summaryNode) + assertEquals(setOf(sinkId), graph.root2SinkFwd.get(summaryId).toSet()) + val nodes = graph.nodesForPathResolution( + sink2Root = intArrayOf(sinkId, summaryId, boundaryId, rootId), + root2Source = intArrayOf(rootId), + ) + assertEquals(listOf(root), nodes.root2Source) + assertEquals(listOf(summaryNode, sink), nodes.root2SinkNoRoot) + } + } + + @Test + fun `alternative full traces preserve every distinct action rule`() { + val node = sinkNode(0) + val trace = TraceResolver.Trace( + entryPointToStart = null, + sourceToSinkTrace = TraceResolver.SourceToSinkTrace( + startNodes = setOf(node), + sinkNodes = setOf(node), + successors = emptyMap(), + ), + ) + + val result = collectActionableRules( + trace = trace, + sinkStatement = node.trace.final.statement, + sinkRules = setOf(sinkRule), + materializeNode = { + listOf( + fullTrace(node, downstreamRules[0]), + fullTrace(node, downstreamRules[1]), + ) + }, + materializeSummary = { error("no nested summary is expected") }, + ) + + val collected = assertIs(result) + val rulesAtAction = collected.rules.getValue(node.trace.final.statement) + assertEquals(setOf(downstreamActions[0]), rulesAtAction.getValue(downstreamRules[0])) + assertEquals(setOf(downstreamActions[1]), rulesAtAction.getValue(downstreamRules[1])) + assertEquals(emptySet(), rulesAtAction.getValue(sinkRule)) + } + + private fun factoredSummaryTrace( + root: TraceResolver.InterProceduralStart2FinalTraceNode, + boundary: TraceResolver.InterProceduralMethodEntryNode, + summaries: List, + sink: TraceResolver.InterProceduralStart2FinalTraceNode, + ): TraceResolver.SourceToSinkTrace { + val successors = linkedMapOf< + TraceResolver.InterProceduralTraceNode, + MutableSet + >() + successors.getOrPut(root, ::linkedSetOf) += call( + root.trace.final.statement, + boundarySummary(boundary), + boundary, + ) + summaries.forEach { summary -> + successors.getOrPut(boundary, ::linkedSetOf) += call( + summary.trace.final.statement, + summary.trace, + summary, + ) + successors.getOrPut(summary, ::linkedSetOf) += call( + summary.trace.final.statement, + SummaryTrace(sink.trace.method, sink.trace.final, sink.trace.traceKind), + sink, + ) + } + return TraceResolver.SourceToSinkTrace( + startNodes = setOf(root), + sinkNodes = setOf(sink), + successors = successors, + ) + } + + private fun sharedBoundaryTrace( + upstream: List, + boundary: TraceResolver.InterProceduralMethodEntryNode, + downstream: List, + ): TraceResolver.SourceToSinkTrace { + val successors = linkedMapOf< + TraceResolver.InterProceduralTraceNode, + MutableSet + >() + upstream.forEach { node -> + successors.getOrPut(node, ::linkedSetOf) += call( + statement = node.trace.final.statement, + summary = boundarySummary(boundary), + node = boundary, + ) + } + downstream.forEach { node -> + successors.getOrPut(boundary, ::linkedSetOf) += call( + statement = node.trace.final.statement, + summary = SummaryTrace(node.trace.method, node.trace.final, node.trace.traceKind), + node = node, + ) + } + return TraceResolver.SourceToSinkTrace( + startNodes = upstream.toSet(), + sinkNodes = downstream.toSet(), + successors = successors, + ) + } + + private fun call( + statement: CommonInst, + summary: SummaryTrace, + node: TraceResolver.InterProceduralTraceNode, + ) = TraceResolver.InterProceduralCall(CallKind.CallToSink, statement, summary, node) + + private fun boundarySummary(boundary: TraceResolver.InterProceduralMethodEntryNode): SummaryTrace { + val fact = boundary.entry.facts.single() + return SummaryTrace( + boundary.entry.entryPoint, + TraceEntry.Final(setOf(TraceEdge.MethodTraceEdge(fact, fact)), boundary.entry.statement), + TraceKind.SummaryTrace, + ) + } + + private fun methodEntryBoundary(): TraceResolver.InterProceduralMethodEntryNode { + val entryPoint = entryPoint("boundary") + val fact = fact(AccessPathBase.This, boundaryMark) + return TraceResolver.InterProceduralMethodEntryNode( + TraceEntry.MethodEntry(setOf(fact), entryPoint) + ) + } + + private fun sourceNode(index: Int): TraceResolver.InterProceduralStart2FinalTraceNode { + val entryPoint = entryPoint("source-$index") + val fact = fact(AccessPathBase.Return, TaintMarkAccessor("source-$index")) + val edge = TraceEdge.SourceTraceEdge(fact) + val source = TraceEntryAction.CallSourceRule( + sourceEdges = setOf(edge), + rule = sourceRules[index], + action = setOf(sourceActions[index]), + ) + return node( + entryPoint, + TraceEntry.SourceStartEntry(null, setOf(source), entryPoint.statement), + TraceEntry.Final(setOf(edge), entryPoint.statement), + ) + } + + private fun sinkNode(index: Int): TraceResolver.InterProceduralStart2FinalTraceNode { + val entryPoint = entryPoint("sink-$index") + return sinkNodeInMethod(index, entryPoint) + } + + private fun sinkNodeInMethod( + index: Int, + entryPoint: MethodEntryPoint, + ): TraceResolver.InterProceduralStart2FinalTraceNode { + val initial = fact(AccessPathBase.Argument(index), boundaryMark) + val final = fact(AccessPathBase.Return, TaintMarkAccessor("sink-$index")) + return node( + entryPoint, + TraceEntry.MethodEntry(setOf(initial), entryPoint), + TraceEntry.Final( + setOf(TraceEdge.MethodTraceEdge(initial, final)), + entryPoint.statement, + ), + ) + } + + private fun node( + entryPoint: MethodEntryPoint, + start: TraceEntry.StartTraceEntry, + final: TraceEntry.Final, + ) = TraceResolver.InterProceduralStart2FinalTraceNode( + Start2FinalTrace(entryPoint, start, final, TraceKind.SummaryTrace) + ) + + private fun fullTrace( + node: TraceResolver.InterProceduralStart2FinalTraceNode, + downstreamRule: TestActionRule?, + ): FullStart2FinalTrace { + val successors = Int2ObjectOpenHashMap() + val entries = if (downstreamRule == null) { + successors[0] = CompactIntSet().also { it.add(1) } + arrayOf(node.trace.startEntry, node.trace.final) + } else { + successors[0] = CompactIntSet().also { it.add(1) } + successors[1] = CompactIntSet().also { it.add(2) } + arrayOf( + node.trace.startEntry, + TraceEntry.Action(node.trace.final.edges, node.trace.final.statement), + node.trace.final, + ) + } + val variants = Int2ObjectOpenHashMap>() + if (downstreamRule != null) { + val index = downstreamRules.indexOf(downstreamRule) + val callRule = TraceEntryAction.CallRule( + edges = node.trace.final.edges, + edgesAfter = node.trace.final.edges, + rule = downstreamRule, + action = setOf(downstreamActions[index]), + ) + variants[1] = listOf( + MethodTraceResolver.ActionVariant( + primaryAction = null, + otherActions = setOf(callRule), + unchanged = emptySet(), + ) + ) + } + return FullStart2FinalTrace( + method = node.trace.method, + entries = entries, + actionVariants = variants, + startEntryId = 0, + finalId = entries.lastIndex, + successors = successors, + traceKind = node.trace.traceKind, + ) + } + + private fun actionBearingSummary(index: Int): ActionBearingSummary { + val entryPoint = entryPoint("nested-summary-$index") + val initial = fact(AccessPathBase.Argument(0), boundaryMark) + val before = fact(AccessPathBase.Return, boundaryMark) + val after = fact(AccessPathBase.Return, TaintMarkAccessor("nested-$index")) + val beforeEdge = TraceEdge.MethodTraceEdge(initial, before) + val afterEdge = TraceEdge.MethodTraceEdge(initial, after) + val summary = SummaryTrace( + entryPoint, + TraceEntry.Final(setOf(afterEdge), entryPoint.statement), + TraceKind.SummaryTrace, + ) + val callSummary = TraceEntryAction.CallSummary( + summaryEdges = setOf( + TraceEntryAction.TraceSummaryEdge.MethodSummary( + edge = beforeEdge, + edgeAfter = afterEdge, + delta = null, + ) + ), + summaryTrace = summary, + ) + + val actionStatement = TestStatement("nested-action-$index", entryPoint.method) + val actionEntry = TraceEntry.Action(setOf(afterEdge), actionStatement) + val callRule = TraceEntryAction.CallRule( + edges = setOf(afterEdge), + edgesAfter = setOf(afterEdge), + rule = downstreamRules[index], + action = setOf(downstreamActions[index]), + ) + val variants = Int2ObjectOpenHashMap>() + variants[1] = listOf( + MethodTraceResolver.ActionVariant( + primaryAction = null, + otherActions = setOf(callRule), + unchanged = emptySet(), + ) + ) + val successors = Int2ObjectOpenHashMap() + successors[0] = CompactIntSet().also { it.add(1) } + successors[1] = CompactIntSet().also { it.add(2) } + val fullTrace = FullStart2FinalTrace( + method = entryPoint, + entries = arrayOf( + TraceEntry.MethodEntry(setOf(initial), entryPoint), + actionEntry, + summary.final, + ), + actionVariants = variants, + startEntryId = 0, + finalId = 2, + successors = successors, + traceKind = TraceKind.SummaryTrace, + ) + return ActionBearingSummary(callSummary, fullTrace) + } + + private fun fact(base: AccessPathBase, mark: TaintMarkAccessor): InitialFactAp = + apManager.mostAbstractInitialAp(base).prependAccessor(mark) + + private fun entryPoint(name: String): MethodEntryPoint { + val method = TestMethod(name) + return MethodEntryPoint(EmptyMethodContext, TestStatement(name, method)) + } + + private data class TestMethod(override val name: String) : CommonMethod { + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + + private data class TestStatement( + val label: String, + val method: CommonMethod, + ) : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod + get() = this@TestStatement.method + } + } + + private data class TestSourceRule(val name: String) : CommonTaintConfigurationSource + private data class TestSourceAction(val name: String) : CommonTaintAssignAction + private data class TestActionRule(val name: String) : CommonTaintConfigurationSource + + private data class ActionBearingSummary( + val action: TraceEntryAction.CallSummary, + val fullTrace: FullStart2FinalTrace, + ) + + private val apManager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + private val boundaryMark = TaintMarkAccessor("boundary") + private val sourceRules = List(upstreamCount) { TestSourceRule("source-rule-$it") } + private val sourceActions = List(upstreamCount) { TestSourceAction("source-action-$it") } + private val downstreamRules = List(downstreamCount) { TestActionRule("downstream-rule-$it") } + private val downstreamActions = List(downstreamCount) { TestSourceAction("downstream-action-$it") } + private val sinkRule: CommonTaintConfigurationItem = object : CommonTaintConfigurationSink { + override val id: String = "sink" + override val meta: CommonTaintConfigurationSinkMeta = object : CommonTaintConfigurationSinkMeta { + override val message: String = "sink" + override val severity = CommonTaintConfigurationSinkMeta.Severity.Error + } + } + + private companion object { + const val upstreamCount = 3 + const val downstreamCount = 2 + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceActionSummaryRelevanceTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceActionSummaryRelevanceTest.kt new file mode 100644 index 000000000..98dcd56bd --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceActionSummaryRelevanceTest.kt @@ -0,0 +1,83 @@ +package org.opentaint.dataflow.ap.ifds.trace.action + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdge +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntryAction.TraceSummaryEdge +import org.opentaint.dataflow.util.Cancellation +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TraceActionSummaryRelevanceTest { + private val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + private val markA = TaintMarkAccessor("a") + private val markB = TaintMarkAccessor("b") + + private fun fact(base: AccessPathBase, mark: TaintMarkAccessor? = null): InitialFactAp { + val fact = manager.mostAbstractInitialAp(base) + return if (mark == null) fact else fact.prependAccessor(mark) + } + + private fun methodSummary( + before: InitialFactAp, + after: InitialFactAp, + ): TraceSummaryEdge.MethodSummary { + val initial = fact(AccessPathBase.Argument(0)) + return TraceSummaryEdge.MethodSummary( + edge = TraceEdge.MethodTraceEdge(initial, before), + edgeAfter = TraceEdge.MethodTraceEdge(initial, after), + delta = null, + ) + } + + @Test + fun `method summary with the same mark is irrelevant`() { + val summary = methodSummary( + before = fact(AccessPathBase.This, markA), + after = fact(AccessPathBase.Return, markA), + ) + + assertFalse(setOf(summary).introducesOrChangesTaintMarks()) + } + + @Test + fun `method summary with a different mark is relevant`() { + val summary = methodSummary( + before = fact(AccessPathBase.This, markA), + after = fact(AccessPathBase.Return, markB), + ) + + assertTrue(setOf(summary).introducesOrChangesTaintMarks()) + } + + @Test + fun `method summary that introduces or removes a mark is relevant`() { + val introduced = methodSummary( + before = fact(AccessPathBase.This), + after = fact(AccessPathBase.Return, markA), + ) + val removed = methodSummary( + before = fact(AccessPathBase.This, markA), + after = fact(AccessPathBase.Return), + ) + + assertTrue(setOf(introduced).introducesOrChangesTaintMarks()) + assertTrue(setOf(removed).introducesOrChangesTaintMarks()) + } + + @Test + fun `source summary is always relevant`() { + val edge = TraceEdge.SourceTraceEdge(fact(AccessPathBase.Return, markA)) + val summary = TraceSummaryEdge.SourceSummary(edge, edge) + + assertTrue(setOf(summary).introducesOrChangesTaintMarks()) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceMarkNodeFilteringTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceMarkNodeFilteringTest.kt new file mode 100644 index 000000000..482cf3e6e --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/trace/action/TraceMarkNodeFilteringTest.kt @@ -0,0 +1,264 @@ +package org.opentaint.dataflow.ap.ifds.trace.action + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.FullStart2FinalTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.Start2FinalTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.SummaryTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdge +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntry +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntryAction +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceKind +import org.opentaint.dataflow.ap.ifds.trace.TraceResolver +import org.opentaint.dataflow.ap.ifds.trace.TraceResolver.CallKind +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.CompactIntSet +import org.opentaint.dataflow.configuration.CommonTaintAssignAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSink +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSinkMeta +import org.opentaint.dataflow.configuration.CommonTaintConfigurationSource +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class TraceMarkNodeFilteringTest { + @Test + fun `unchanged marks and covered zero start skip full trace resolution`() { + val entryPoint = MethodEntryPoint(EmptyMethodContext, statement) + val startFact = fact(AccessPathBase.Argument(0), markA) + val predecessorFinalFact = fact(AccessPathBase.This, markA) + val zeroFinalFact = fact(AccessPathBase.Return, markA) + val predecessor = node( + entryPoint, + TraceEntry.MethodEntry(setOf(startFact), entryPoint), + TraceEntry.Final( + setOf(TraceEdge.MethodTraceEdge(startFact, predecessorFinalFact)), + statement, + ), + ) + val zero = node( + entryPoint, + TraceEntry.SourceStartEntry(null, emptySet(), statement), + TraceEntry.Final(setOf(TraceEdge.SourceTraceEdge(zeroFinalFact)), statement), + ) + var materializations = 0 + + val result = collectActionableRules( + trace = sinkBranchTrace(predecessor, zero), + sinkStatement = statement, + sinkRules = setOf(sinkRule), + materializeNode = { + materializations++ + listOf(fullTrace(it as TraceResolver.InterProceduralStart2FinalTraceNode)) + }, + materializeSummary = { emptyList() }, + ) + + assertIs(result) + assertEquals(0, materializations) + } + + @Test + fun `different marks retain full trace resolution`() { + val entryPoint = MethodEntryPoint(EmptyMethodContext, statement) + val startFact = fact(AccessPathBase.Argument(0), markA) + val predecessorFinalFact = fact(AccessPathBase.This, markB) + val zeroFinalFact = fact(AccessPathBase.Return, markA) + val predecessor = node( + entryPoint, + TraceEntry.MethodEntry(setOf(startFact), entryPoint), + TraceEntry.Final( + setOf(TraceEdge.MethodTraceEdge(startFact, predecessorFinalFact)), + statement, + ), + ) + val zero = node( + entryPoint, + TraceEntry.SourceStartEntry(null, emptySet(), statement), + TraceEntry.Final(setOf(TraceEdge.SourceTraceEdge(zeroFinalFact)), statement), + ) + var materializations = 0 + + val result = collectActionableRules( + trace = sinkBranchTrace(predecessor, zero), + sinkStatement = statement, + sinkRules = setOf(sinkRule), + materializeNode = { + materializations++ + listOf(fullTrace(it as TraceResolver.InterProceduralStart2FinalTraceNode)) + }, + materializeSummary = { emptyList() }, + ) + + assertIs(result) + assertEquals(2, materializations) + } + + @Test + fun `covered zero start on source branch keeps its shallow source action`() { + val entryPoint = MethodEntryPoint(EmptyMethodContext, statement) + val finalFact = fact(AccessPathBase.Return, markA) + val sourceEdge = TraceEdge.SourceTraceEdge(finalFact) + val source = TraceEntryAction.CallSourceRule( + sourceEdges = setOf(sourceEdge), + rule = sourceRule, + action = setOf(sourceAction), + ) + val current = node( + entryPoint, + TraceEntry.SourceStartEntry(null, setOf(source), statement), + TraceEntry.Final(setOf(sourceEdge), statement), + ) + val summary = SummaryTrace(current.trace.method, current.trace.final, current.trace.traceKind) + val predecessor = node( + entryPoint, + TraceEntry.SourceStartEntry( + TraceEntryAction.CallSourceSummary( + summaryEdges = setOf( + TraceEntryAction.TraceSummaryEdge.SourceSummary(sourceEdge, sourceEdge) + ), + summaryTrace = summary, + ), + emptySet(), + statement, + ), + TraceEntry.Final(setOf(sourceEdge), statement), + ) + val materialized = mutableListOf() + + val result = collectActionableRules( + trace = sourceBranchTrace(predecessor, current, summary), + sinkStatement = statement, + sinkRules = setOf(sinkRule), + materializeNode = { traceNode -> + materialized += traceNode + listOf(fullTrace(traceNode as TraceResolver.InterProceduralStart2FinalTraceNode)) + }, + materializeSummary = { emptyList() }, + ) + + assertIs(result) + assertEquals(listOf(predecessor), materialized) + assertEquals(setOf(sourceAction), result.rules.getValue(statement).getValue(sourceRule)) + } + + private fun fact(base: AccessPathBase, mark: TaintMarkAccessor): InitialFactAp = + apManager.mostAbstractInitialAp(base).prependAccessor(mark) + + private fun node( + entryPoint: MethodEntryPoint, + start: TraceEntry.StartTraceEntry, + final: TraceEntry.Final, + ): TraceResolver.InterProceduralStart2FinalTraceNode = + TraceResolver.InterProceduralStart2FinalTraceNode( + Start2FinalTrace(entryPoint, start, final, TraceKind.SummaryTrace) + ) + + private fun sinkBranchTrace( + predecessor: TraceResolver.InterProceduralStart2FinalTraceNode, + current: TraceResolver.InterProceduralStart2FinalTraceNode, + ): TraceResolver.Trace { + val call = TraceResolver.InterProceduralCall( + kind = CallKind.CallToSink, + statement = predecessor.trace.final.statement, + summary = SummaryTrace(current.trace.method, current.trace.final, current.trace.traceKind), + node = current, + ) + return TraceResolver.Trace( + entryPointToStart = null, + sourceToSinkTrace = TraceResolver.SourceToSinkTrace( + startNodes = setOf(predecessor), + sinkNodes = setOf(current), + successors = mapOf(predecessor to setOf(call)), + ), + ) + } + + private fun sourceBranchTrace( + predecessor: TraceResolver.InterProceduralStart2FinalTraceNode, + current: TraceResolver.InterProceduralStart2FinalTraceNode, + summary: SummaryTrace, + ): TraceResolver.Trace { + val call = TraceResolver.InterProceduralCall( + kind = CallKind.CallToSource, + statement = predecessor.trace.startEntry.statement, + summary = summary, + node = current, + ) + return TraceResolver.Trace( + entryPointToStart = null, + sourceToSinkTrace = TraceResolver.SourceToSinkTrace( + startNodes = setOf(predecessor), + sinkNodes = setOf(predecessor), + successors = mapOf(predecessor to setOf(call)), + ), + ) + } + + private fun fullTrace( + node: TraceResolver.InterProceduralStart2FinalTraceNode, + ): FullStart2FinalTrace { + val successors = Int2ObjectOpenHashMap() + successors[0] = CompactIntSet().also { it.add(1) } + return FullStart2FinalTrace( + method = node.trace.method, + entries = arrayOf(node.trace.startEntry, node.trace.final), + actionVariants = Int2ObjectOpenHashMap(), + startEntryId = 0, + finalId = 1, + successors = successors, + traceKind = node.trace.traceKind, + ) + } + + private val apManager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + private val markA = TaintMarkAccessor("a") + private val markB = TaintMarkAccessor("b") + private val sinkRule = object : CommonTaintConfigurationSink { + override val id: String = "sink" + override val meta: CommonTaintConfigurationSinkMeta = object : CommonTaintConfigurationSinkMeta { + override val message: String = "sink" + override val severity: CommonTaintConfigurationSinkMeta.Severity = + CommonTaintConfigurationSinkMeta.Severity.Error + } + } + private val sourceRule = object : CommonTaintConfigurationSource {} + private val sourceAction = object : CommonTaintAssignAction {} + private val method: CommonMethod = object : CommonMethod { + override val name: String = "test" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = listOf(statement) + override val entries: List = listOf(statement) + override val exits: List = listOf(statement) + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } + private val statement: CommonInst = object : CommonInst { + override val location: CommonInstLocation = object : CommonInstLocation { + override val method: CommonMethod + get() = this@TraceMarkNodeFilteringTest.method + } + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/BaseOnlyCleanerDeduplicationTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/BaseOnlyCleanerDeduplicationTest.kt new file mode 100644 index 000000000..91756fc0b --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/BaseOnlyCleanerDeduplicationTest.kt @@ -0,0 +1,41 @@ +package org.opentaint.dataflow.taint + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.util.Cancellation +import kotlin.test.Test +import kotlin.test.assertEquals + +class BaseOnlyCleanerDeduplicationTest { + @Test + fun `clearing a mark does not duplicate an implicit Any branch`() { + val manager = BaseOnlyApManager( + AnyAccessorUnrollStrategy.AnyAccessorDisabled, + Cancellation(), + fieldSensitive = true, + ) + val base = AccessPathBase.Argument(0) + val mark = TaintMarkAccessor("mark") + val fact = manager.createFinalAp(base, ExclusionSet.Empty).prependAccessor(mark) + val reader = FinalFactReader(fact, manager) + val initial = EvaluatedCleanAction.initial(reader) + val rule = object : CommonTaintConfigurationItem {} + val action = object : CommonTaintAction {} + + val result = TaintCleanActionEvaluator().removeFinalFact( + initial, + PositionAccess.Simple(base), + mark, + rule, + action, + ) + + assertEquals(1, result.size) + assertEquals(fact, result.single().fact?.factAp) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/CancellationTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/CancellationTest.kt new file mode 100644 index 000000000..1032e8c8d --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/CancellationTest.kt @@ -0,0 +1,46 @@ +package org.opentaint.dataflow.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CancellationTest { + @Test + fun derivedCancellationRequiresParentAndAdditionalCondition() { + val parent = Cancellation() + var condition = true + val derived = parent.derive { condition } + + assertTrue(derived.isActive()) + condition = false + assertFalse(derived.isActive()) + + condition = true + parent.cancel() + assertFalse(derived.isActive()) + } + + @Test + fun cancelledCheckpointDoesNotCancelParentCoroutineScope() = runBlocking { + val parent = Job() + val scope = CoroutineScope(coroutineContext + parent) + val cancellation = Cancellation().also { it.cancel() } + + val child = scope.launch { + cancellation.checkpoint() + } + child.join() + + assertTrue(child.isCancelled) + assertFalse(child.isActive) + assertTrue(parent.isActive) + + parent.cancelAndJoin() + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/ConcurrentReadSafeLongCollectionsTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/ConcurrentReadSafeLongCollectionsTest.kt new file mode 100644 index 000000000..bb03bbe8b --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/ConcurrentReadSafeLongCollectionsTest.kt @@ -0,0 +1,156 @@ +package org.opentaint.dataflow.util + +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.thread +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ConcurrentReadSafeLongCollectionsTest { + @Test + fun `object map supports concurrent reads while single writer grows`() { + val map = object2IntMap() + val done = AtomicBoolean(false) + val start = CountDownLatch(1) + val failures = ConcurrentLinkedQueue() + + val readers = List(READER_COUNT) { + thread(name = "object-map-reader-$it", isDaemon = true) { + start.await() + try { + while (!done.get()) { + val value = map.getInt(PROBE_KEY.toInt()) + assertTrue( + value == ConcurrentReadSafeObject2IntMap.NO_VALUE || value == PROBE_KEY.toInt(), + "observed a partially published value: $value", + ) + } + } catch (failure: Throwable) { + failures.add(failure) + } + } + } + + val writer = thread(name = "object-map-writer", isDaemon = true) { + start.await() + try { + for (key in 1..ENTRY_COUNT) map.put(key, key) + } catch (failure: Throwable) { + failures.add(failure) + } finally { + done.set(true) + } + } + + start.countDown() + writer.join(10_000) + assertFalse(writer.isAlive, "writer did not finish") + readers.forEach { it.join(10_000) } + assertTrue(readers.none(Thread::isAlive), "a reader did not observe the completed write") + + assertTrue(failures.isEmpty(), failures.joinToString("\n") { it.stackTraceToString() }) + assertEquals(ENTRY_COUNT, map.size) + assertEquals(PROBE_KEY.toInt(), map.getInt(PROBE_KEY.toInt())) + } + + @Test + fun `long map supports concurrent reads while single writer rehashes`() { + val map = long2ObjectMap() + val done = AtomicBoolean(false) + val start = CountDownLatch(1) + val failures = ConcurrentLinkedQueue() + + val readers = List(READER_COUNT) { + thread(name = "long-map-reader-$it") { + start.await() + try { + while (!done.get()) { + map.forEachEntry { key, value -> assertEquals(key, value) } + map[PROBE_KEY] + } + } catch (failure: Throwable) { + failures.add(failure) + } + } + } + + val writer = thread(name = "long-map-writer") { + start.await() + try { + map.put(0, 0) + for (key in 1L..ENTRY_COUNT.toLong()) { + map.put(key, key) + } + } catch (failure: Throwable) { + failures.add(failure) + } finally { + done.set(true) + } + } + + start.countDown() + writer.join() + readers.forEach(Thread::join) + + assertTrue(failures.isEmpty(), failures.joinToString("\n") { it.stackTraceToString() }) + val collected = HashMap() + map.forEachEntry { key, value -> collected[key] = value } + assertEquals(ENTRY_COUNT + 1, collected.size) + assertEquals(PROBE_KEY, collected[PROBE_KEY]) + } + + @Test + fun `long set supports concurrent reads while single writer rehashes`() { + val set = longSet() + val done = AtomicBoolean(false) + val start = CountDownLatch(1) + val failures = ConcurrentLinkedQueue() + + val readers = List(READER_COUNT) { + thread(name = "long-set-reader-$it") { + start.await() + try { + while (!done.get()) { + set.forEachLong { value -> assertTrue(value in 0L..ENTRY_COUNT.toLong()) } + set.contains(PROBE_KEY) + } + } catch (failure: Throwable) { + failures.add(failure) + } + } + } + + val writer = thread(name = "long-set-writer") { + start.await() + try { + set.add(0) + for (value in 1L..ENTRY_COUNT.toLong()) { + set.add(value) + } + } catch (failure: Throwable) { + failures.add(failure) + } finally { + done.set(true) + } + } + + start.countDown() + writer.join() + readers.forEach(Thread::join) + + assertTrue(failures.isEmpty(), failures.joinToString("\n") { it.stackTraceToString() }) + val collected = HashSet() + set.forEachLong(collected::add) + assertEquals(ENTRY_COUNT + 1, collected.size) + assertTrue(PROBE_KEY in collected) + } + + private companion object { + const val ENTRY_COUNT = 100_000 + const val READER_COUNT = 4 + const val PROBE_KEY = 73_421L + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/MemoryManagerTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/MemoryManagerTest.kt new file mode 100644 index 000000000..13e0a8bcf --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/MemoryManagerTest.kt @@ -0,0 +1,19 @@ +package org.opentaint.dataflow.util + +import java.lang.management.ManagementFactory +import kotlin.test.Test +import kotlin.test.assertEquals + +class MemoryManagerTest { + @Test + fun `each analysis run gets independent memory pressure state`() { + val manager = MemoryManager(RefManager(), memoryThreshold = 0.9) {} + val memory = ManagementFactory.getMemoryMXBean() + val firstRun = manager.GCNotificationListener(memory) + val secondRun = manager.GCNotificationListener(memory) + + firstRun.memoryManagerState.set(MemoryManager.State.GcAfterCleanup) + + assertEquals(MemoryManager.State.Normal, secondRun.memoryManagerState.get()) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode0.golden.txt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode0.golden.txt new file mode 100644 index 000000000..380bf6279 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode0.golden.txt @@ -0,0 +1,95 @@ +================================================================ +BASE-ONLY contains PIN — mode fieldSensitive=false +cell = F_row(final).contains(F_col(initial)); T = contained, . = not +contains(i) = sameBase && containsProjected(access, i.access) [directional coverage plus the documented missing-structural projection match] +================================================================ + +## FACTS (16) + F00 = x.* (-1 -1 * ) [ap@2] + F01 = x.$ (-1 -1 $ ) [value] + F02 = x.!t1.$ (-1 -1 t1 ) [mark] + F03 = x.!t2.$ (-1 -1 t2 ) [mark] + F04 = x.s1.* (s1 -1 * ) [ap@2] + F05 = x.s1.$ (s1 -1 $ ) [value] + F06 = x.s1.!t1.$ (s1 -1 t1 ) [mark] + F07 = x.s1.!t2.$ (s1 -1 t2 ) [mark] + F08 = x.s2.* (s2 -1 * ) [ap@2] + F09 = x.s2.$ (s2 -1 $ ) [value] + F10 = x.s2.!t1.$ (s2 -1 t1 ) [mark] + F11 = x.s2.!t2.$ (s2 -1 t2 ) [mark] + F12 = x.*s (* -1 -1 ) [ap@0] + F13 = x.*f (-1 * -1 ) [ap@1] + F14 = x.s1.*f (s1 * -1 ) [ap@1] + F15 = x.s2.*f (s2 * -1 ) [ap@1] + +## CONTAINS MATRIX cell = F_row.contains(F_col) + F00 F01 F02 F03 F04 F05 F06 F07 F08 F09 F10 F11 F12 F13 F14 F15 + F00 T T T T . . . . . . . . . T . . + F01 . T . . . . . . . . . . . . . . + F02 . . T . . . . . . . . . . . . . + F03 . . . T . . . . . . . . . . . . + F04 . . . . T T T T . . . . . . T . + F05 . . . . . T . . . . . . . . . . + F06 . . . . . . T . . . . . . . . . + F07 . . . . . . . T . . . . . . . . + F08 . . . . . . . . T T T T . . . T + F09 . . . . . . . . . T . . . . . . + F10 . . . . . . . . . . T . . . . . + F11 . . . . . . . . . . . T . . . . + F12 T T T T T T T T T T T T T T T T + F13 T T T T . . . . . . . . . T . . + F14 . . . . T T T T . . . . . . T . + F15 . . . . . . . . T T T T . . . T + +## PER-FACT BREAKDOWN (initials each final contains; self omitted) + x.* contains: x.$, x.!t1.$, x.!t2.$, x.*f + x.s1.* contains: x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s1.*f + x.s2.* contains: x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.s2.*f + x.*s contains: x.*, x.$, x.!t1.$, x.!t2.$, x.s1.*, x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s2.*, x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.*f, x.s1.*f, x.s2.*f + x.*f contains: x.*, x.$, x.!t1.$, x.!t2.$ + x.s1.*f contains: x.s1.*, x.s1.$, x.s1.!t1.$, x.s1.!t2.$ + x.s2.*f contains: x.s2.*, x.s2.$, x.s2.!t1.$, x.s2.!t2.$ + +## OFF-DIAGONAL TRUE CELLS (mechanism) + x.* contains x.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.*f : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.*f : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.*f : containsAccess(abstract-prefix wildcard) + x.*s contains x.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.*f : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.*f : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.*f : containsAccess(abstract-prefix wildcard) + x.*f contains x.* : containsAccess(abstract-prefix wildcard) + x.*f contains x.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.* : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.* : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + +## CROSS-BASE PROBE x-fact.contains(y-same-access) + cross-base identical-access contained count = 0 / 16 diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode1.golden.txt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode1.golden.txt new file mode 100644 index 000000000..6d49bec27 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode1.golden.txt @@ -0,0 +1,437 @@ +================================================================ +BASE-ONLY contains PIN — mode fieldSensitive=true +cell = F_row(final).contains(F_col(initial)); T = contained, . = not +contains(i) = sameBase && containsProjected(access, i.access) [directional coverage plus the documented missing-structural projection match] +================================================================ + +## FACTS (52) + F00 = x.* (-1 -1 * ) [ap@2] + F01 = x.$ (-1 -1 $ ) [value] + F02 = x.!t1.$ (-1 -1 t1 ) [mark] + F03 = x.!t2.$ (-1 -1 t2 ) [mark] + F04 = x.f1.* (-1 f1 * ) [ap@2] + F05 = x.f1.$ (-1 f1 $ ) [value] + F06 = x.f1.!t1.$ (-1 f1 t1 ) [mark] + F07 = x.f1.!t2.$ (-1 f1 t2 ) [mark] + F08 = x.f2.* (-1 f2 * ) [ap@2] + F09 = x.f2.$ (-1 f2 $ ) [value] + F10 = x.f2.!t1.$ (-1 f2 t1 ) [mark] + F11 = x.f2.!t2.$ (-1 f2 t2 ) [mark] + F12 = x.[el].* (-1 [el] * ) [ap@2] + F13 = x.[el].$ (-1 [el] $ ) [value] + F14 = x.[el].!t1.$ (-1 [el] t1 ) [mark] + F15 = x.[el].!t2.$ (-1 [el] t2 ) [mark] + F16 = x.s1.* (s1 -1 * ) [ap@2] + F17 = x.s1.$ (s1 -1 $ ) [value] + F18 = x.s1.!t1.$ (s1 -1 t1 ) [mark] + F19 = x.s1.!t2.$ (s1 -1 t2 ) [mark] + F20 = x.s1.f1.* (s1 f1 * ) [ap@2] + F21 = x.s1.f1.$ (s1 f1 $ ) [value] + F22 = x.s1.f1.!t1.$ (s1 f1 t1 ) [mark] + F23 = x.s1.f1.!t2.$ (s1 f1 t2 ) [mark] + F24 = x.s1.f2.* (s1 f2 * ) [ap@2] + F25 = x.s1.f2.$ (s1 f2 $ ) [value] + F26 = x.s1.f2.!t1.$ (s1 f2 t1 ) [mark] + F27 = x.s1.f2.!t2.$ (s1 f2 t2 ) [mark] + F28 = x.s1.[el].* (s1 [el] * ) [ap@2] + F29 = x.s1.[el].$ (s1 [el] $ ) [value] + F30 = x.s1.[el].!t1.$ (s1 [el] t1 ) [mark] + F31 = x.s1.[el].!t2.$ (s1 [el] t2 ) [mark] + F32 = x.s2.* (s2 -1 * ) [ap@2] + F33 = x.s2.$ (s2 -1 $ ) [value] + F34 = x.s2.!t1.$ (s2 -1 t1 ) [mark] + F35 = x.s2.!t2.$ (s2 -1 t2 ) [mark] + F36 = x.s2.f1.* (s2 f1 * ) [ap@2] + F37 = x.s2.f1.$ (s2 f1 $ ) [value] + F38 = x.s2.f1.!t1.$ (s2 f1 t1 ) [mark] + F39 = x.s2.f1.!t2.$ (s2 f1 t2 ) [mark] + F40 = x.s2.f2.* (s2 f2 * ) [ap@2] + F41 = x.s2.f2.$ (s2 f2 $ ) [value] + F42 = x.s2.f2.!t1.$ (s2 f2 t1 ) [mark] + F43 = x.s2.f2.!t2.$ (s2 f2 t2 ) [mark] + F44 = x.s2.[el].* (s2 [el] * ) [ap@2] + F45 = x.s2.[el].$ (s2 [el] $ ) [value] + F46 = x.s2.[el].!t1.$ (s2 [el] t1 ) [mark] + F47 = x.s2.[el].!t2.$ (s2 [el] t2 ) [mark] + F48 = x.*s (* -1 -1 ) [ap@0] + F49 = x.*f (-1 * -1 ) [ap@1] + F50 = x.s1.*f (s1 * -1 ) [ap@1] + F51 = x.s2.*f (s2 * -1 ) [ap@1] + +## CONTAINS MATRIX cell = F_row.contains(F_col) + F00 F01 F02 F03 F04 F05 F06 F07 F08 F09 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31 F32 F33 F34 F35 F36 F37 F38 F39 F40 F41 F42 F43 F44 F45 F46 F47 F48 F49 F50 F51 + F00 T T T T T T T T T T T T T T T T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . + F01 . T . . . T . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F02 . . T . . . T . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F03 . . . T . . . T . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F04 T T T T T T T T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F05 . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F06 . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F07 . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F08 T T T T . . . . T T T T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F09 . T . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F10 . . T . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F11 . . . T . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F12 T T T T . . . . . . . . T T T T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F13 . T . . . . . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F14 . . T . . . . . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F15 . . . T . . . . . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F16 . . . . . . . . . . . . . . . . T T T T T T T T T T T T T T T T . . . . . . . . . . . . . . . . . . T . + F17 . . . . . . . . . . . . . . . . . T . . . T . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . + F18 . . . . . . . . . . . . . . . . . . T . . . T . . . T . . . T . . . . . . . . . . . . . . . . . . . . . + F19 . . . . . . . . . . . . . . . . . . . T . . . T . . . T . . . T . . . . . . . . . . . . . . . . . . . . + F20 . . . . . . . . . . . . . . . . T T T T T T T T . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F21 . . . . . . . . . . . . . . . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F22 . . . . . . . . . . . . . . . . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F23 . . . . . . . . . . . . . . . . . . . T . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F24 . . . . . . . . . . . . . . . . T T T T . . . . T T T T . . . . . . . . . . . . . . . . . . . . . . . . + F25 . . . . . . . . . . . . . . . . . T . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . . + F26 . . . . . . . . . . . . . . . . . . T . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . . + F27 . . . . . . . . . . . . . . . . . . . T . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . . . + F28 . . . . . . . . . . . . . . . . T T T T . . . . . . . . T T T T . . . . . . . . . . . . . . . . . . . . + F29 . . . . . . . . . . . . . . . . . T . . . . . . . . . . . T . . . . . . . . . . . . . . . . . . . . . . + F30 . . . . . . . . . . . . . . . . . . T . . . . . . . . . . . T . . . . . . . . . . . . . . . . . . . . . + F31 . . . . . . . . . . . . . . . . . . . T . . . . . . . . . . . T . . . . . . . . . . . . . . . . . . . . + F32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T T T T T T T T T T T T T T T T . . . T + F33 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . T . . . T . . . T . . . . . . + F34 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . T . . . T . . . T . . . . . + F35 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . T . . . T . . . T . . . . + F36 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T T T T T T T T . . . . . . . . . . . . + F37 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . T . . . . . . . . . . . . . . + F38 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . T . . . . . . . . . . . . . + F39 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . T . . . . . . . . . . . . + F40 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T T T T . . . . T T T T . . . . . . . . + F41 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . . . . . T . . . . . . . . . . + F42 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . . . . . T . . . . . . . . . + F43 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . . . . . T . . . . . . . . + F44 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T T T T . . . . . . . . T T T T . . . . + F45 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . . . . . . . . . T . . . . . . + F46 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . . . . . . . . . T . . . . . + F47 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . . . . . . . . . . T . . . . + F48 T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T T + F49 T T T T T T T T T T T T T T T T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T . . + F50 . . . . . . . . . . . . . . . . T T T T T T T T T T T T T T T T . . . . . . . . . . . . . . . . . . T . + F51 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . T T T T T T T T T T T T T T T T . . . T + +## PER-FACT BREAKDOWN (initials each final contains; self omitted) + x.* contains: x.$, x.!t1.$, x.!t2.$, x.f1.*, x.f1.$, x.f1.!t1.$, x.f1.!t2.$, x.f2.*, x.f2.$, x.f2.!t1.$, x.f2.!t2.$, x.[el].*, x.[el].$, x.[el].!t1.$, x.[el].!t2.$, x.*f + x.$ contains: x.f1.$, x.f2.$, x.[el].$ + x.!t1.$ contains: x.f1.!t1.$, x.f2.!t1.$, x.[el].!t1.$ + x.!t2.$ contains: x.f1.!t2.$, x.f2.!t2.$, x.[el].!t2.$ + x.f1.* contains: x.*, x.$, x.!t1.$, x.!t2.$, x.f1.$, x.f1.!t1.$, x.f1.!t2.$ + x.f1.$ contains: x.$ + x.f1.!t1.$ contains: x.!t1.$ + x.f1.!t2.$ contains: x.!t2.$ + x.f2.* contains: x.*, x.$, x.!t1.$, x.!t2.$, x.f2.$, x.f2.!t1.$, x.f2.!t2.$ + x.f2.$ contains: x.$ + x.f2.!t1.$ contains: x.!t1.$ + x.f2.!t2.$ contains: x.!t2.$ + x.[el].* contains: x.*, x.$, x.!t1.$, x.!t2.$, x.[el].$, x.[el].!t1.$, x.[el].!t2.$ + x.[el].$ contains: x.$ + x.[el].!t1.$ contains: x.!t1.$ + x.[el].!t2.$ contains: x.!t2.$ + x.s1.* contains: x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s1.f1.*, x.s1.f1.$, x.s1.f1.!t1.$, x.s1.f1.!t2.$, x.s1.f2.*, x.s1.f2.$, x.s1.f2.!t1.$, x.s1.f2.!t2.$, x.s1.[el].*, x.s1.[el].$, x.s1.[el].!t1.$, x.s1.[el].!t2.$, x.s1.*f + x.s1.$ contains: x.s1.f1.$, x.s1.f2.$, x.s1.[el].$ + x.s1.!t1.$ contains: x.s1.f1.!t1.$, x.s1.f2.!t1.$, x.s1.[el].!t1.$ + x.s1.!t2.$ contains: x.s1.f1.!t2.$, x.s1.f2.!t2.$, x.s1.[el].!t2.$ + x.s1.f1.* contains: x.s1.*, x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s1.f1.$, x.s1.f1.!t1.$, x.s1.f1.!t2.$ + x.s1.f1.$ contains: x.s1.$ + x.s1.f1.!t1.$ contains: x.s1.!t1.$ + x.s1.f1.!t2.$ contains: x.s1.!t2.$ + x.s1.f2.* contains: x.s1.*, x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s1.f2.$, x.s1.f2.!t1.$, x.s1.f2.!t2.$ + x.s1.f2.$ contains: x.s1.$ + x.s1.f2.!t1.$ contains: x.s1.!t1.$ + x.s1.f2.!t2.$ contains: x.s1.!t2.$ + x.s1.[el].* contains: x.s1.*, x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s1.[el].$, x.s1.[el].!t1.$, x.s1.[el].!t2.$ + x.s1.[el].$ contains: x.s1.$ + x.s1.[el].!t1.$ contains: x.s1.!t1.$ + x.s1.[el].!t2.$ contains: x.s1.!t2.$ + x.s2.* contains: x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.s2.f1.*, x.s2.f1.$, x.s2.f1.!t1.$, x.s2.f1.!t2.$, x.s2.f2.*, x.s2.f2.$, x.s2.f2.!t1.$, x.s2.f2.!t2.$, x.s2.[el].*, x.s2.[el].$, x.s2.[el].!t1.$, x.s2.[el].!t2.$, x.s2.*f + x.s2.$ contains: x.s2.f1.$, x.s2.f2.$, x.s2.[el].$ + x.s2.!t1.$ contains: x.s2.f1.!t1.$, x.s2.f2.!t1.$, x.s2.[el].!t1.$ + x.s2.!t2.$ contains: x.s2.f1.!t2.$, x.s2.f2.!t2.$, x.s2.[el].!t2.$ + x.s2.f1.* contains: x.s2.*, x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.s2.f1.$, x.s2.f1.!t1.$, x.s2.f1.!t2.$ + x.s2.f1.$ contains: x.s2.$ + x.s2.f1.!t1.$ contains: x.s2.!t1.$ + x.s2.f1.!t2.$ contains: x.s2.!t2.$ + x.s2.f2.* contains: x.s2.*, x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.s2.f2.$, x.s2.f2.!t1.$, x.s2.f2.!t2.$ + x.s2.f2.$ contains: x.s2.$ + x.s2.f2.!t1.$ contains: x.s2.!t1.$ + x.s2.f2.!t2.$ contains: x.s2.!t2.$ + x.s2.[el].* contains: x.s2.*, x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.s2.[el].$, x.s2.[el].!t1.$, x.s2.[el].!t2.$ + x.s2.[el].$ contains: x.s2.$ + x.s2.[el].!t1.$ contains: x.s2.!t1.$ + x.s2.[el].!t2.$ contains: x.s2.!t2.$ + x.*s contains: x.*, x.$, x.!t1.$, x.!t2.$, x.f1.*, x.f1.$, x.f1.!t1.$, x.f1.!t2.$, x.f2.*, x.f2.$, x.f2.!t1.$, x.f2.!t2.$, x.[el].*, x.[el].$, x.[el].!t1.$, x.[el].!t2.$, x.s1.*, x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s1.f1.*, x.s1.f1.$, x.s1.f1.!t1.$, x.s1.f1.!t2.$, x.s1.f2.*, x.s1.f2.$, x.s1.f2.!t1.$, x.s1.f2.!t2.$, x.s1.[el].*, x.s1.[el].$, x.s1.[el].!t1.$, x.s1.[el].!t2.$, x.s2.*, x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.s2.f1.*, x.s2.f1.$, x.s2.f1.!t1.$, x.s2.f1.!t2.$, x.s2.f2.*, x.s2.f2.$, x.s2.f2.!t1.$, x.s2.f2.!t2.$, x.s2.[el].*, x.s2.[el].$, x.s2.[el].!t1.$, x.s2.[el].!t2.$, x.*f, x.s1.*f, x.s2.*f + x.*f contains: x.*, x.$, x.!t1.$, x.!t2.$, x.f1.*, x.f1.$, x.f1.!t1.$, x.f1.!t2.$, x.f2.*, x.f2.$, x.f2.!t1.$, x.f2.!t2.$, x.[el].*, x.[el].$, x.[el].!t1.$, x.[el].!t2.$ + x.s1.*f contains: x.s1.*, x.s1.$, x.s1.!t1.$, x.s1.!t2.$, x.s1.f1.*, x.s1.f1.$, x.s1.f1.!t1.$, x.s1.f1.!t2.$, x.s1.f2.*, x.s1.f2.$, x.s1.f2.!t1.$, x.s1.f2.!t2.$, x.s1.[el].*, x.s1.[el].$, x.s1.[el].!t1.$, x.s1.[el].!t2.$ + x.s2.*f contains: x.s2.*, x.s2.$, x.s2.!t1.$, x.s2.!t2.$, x.s2.f1.*, x.s2.f1.$, x.s2.f1.!t1.$, x.s2.f1.!t2.$, x.s2.f2.*, x.s2.f2.$, x.s2.f2.!t1.$, x.s2.f2.!t2.$, x.s2.[el].*, x.s2.[el].$, x.s2.[el].!t1.$, x.s2.[el].!t2.$ + +## OFF-DIAGONAL TRUE CELLS (mechanism) + x.* contains x.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.f1.* : containsAccess(abstract-prefix wildcard) + x.* contains x.f1.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.f2.* : containsAccess(abstract-prefix wildcard) + x.* contains x.f2.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.[el].* : containsAccess(abstract-prefix wildcard) + x.* contains x.[el].$ : containsAccess(abstract-prefix wildcard) + x.* contains x.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.* contains x.*f : containsAccess(abstract-prefix wildcard) + x.$ contains x.f1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.$ contains x.f2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.$ contains x.[el].$ : covers(directional virtual field-[any]; suffix+static exact) + x.!t1.$ contains x.f1.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.!t1.$ contains x.f2.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.!t1.$ contains x.[el].!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.!t2.$ contains x.f1.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.!t2.$ contains x.f2.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.!t2.$ contains x.[el].!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.f1.* contains x.* : containsAccess(abstract-prefix wildcard) + x.f1.* contains x.$ : containsAccess(abstract-prefix wildcard) + x.f1.* contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.f1.* contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.f1.* contains x.f1.$ : containsAccess(abstract-prefix wildcard) + x.f1.* contains x.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.f1.* contains x.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.f1.$ contains x.$ : covers(directional virtual field-[any]; suffix+static exact) + x.f1.!t1.$ contains x.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.f1.!t2.$ contains x.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.f2.* contains x.* : containsAccess(abstract-prefix wildcard) + x.f2.* contains x.$ : containsAccess(abstract-prefix wildcard) + x.f2.* contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.f2.* contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.f2.* contains x.f2.$ : containsAccess(abstract-prefix wildcard) + x.f2.* contains x.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.f2.* contains x.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.f2.$ contains x.$ : covers(directional virtual field-[any]; suffix+static exact) + x.f2.!t1.$ contains x.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.f2.!t2.$ contains x.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.[el].* contains x.* : containsAccess(abstract-prefix wildcard) + x.[el].* contains x.$ : containsAccess(abstract-prefix wildcard) + x.[el].* contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.[el].* contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.[el].* contains x.[el].$ : containsAccess(abstract-prefix wildcard) + x.[el].* contains x.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.[el].* contains x.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.[el].$ contains x.$ : covers(directional virtual field-[any]; suffix+static exact) + x.[el].!t1.$ contains x.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.[el].!t2.$ contains x.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.* contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f1.* : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f2.* : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f2.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.[el].* : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.[el].$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.* contains x.s1.*f : containsAccess(abstract-prefix wildcard) + x.s1.$ contains x.s1.f1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.$ contains x.s1.f2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.$ contains x.s1.[el].$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.!t1.$ contains x.s1.f1.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.!t1.$ contains x.s1.f2.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.!t1.$ contains x.s1.[el].!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.!t2.$ contains x.s1.f1.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.!t2.$ contains x.s1.f2.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.!t2.$ contains x.s1.[el].!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.f1.* contains x.s1.* : containsAccess(abstract-prefix wildcard) + x.s1.f1.* contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.s1.f1.* contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.f1.* contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.f1.* contains x.s1.f1.$ : containsAccess(abstract-prefix wildcard) + x.s1.f1.* contains x.s1.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.f1.* contains x.s1.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.f1.$ contains x.s1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.f1.!t1.$ contains x.s1.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.f1.!t2.$ contains x.s1.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.f2.* contains x.s1.* : containsAccess(abstract-prefix wildcard) + x.s1.f2.* contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.s1.f2.* contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.f2.* contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.f2.* contains x.s1.f2.$ : containsAccess(abstract-prefix wildcard) + x.s1.f2.* contains x.s1.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.f2.* contains x.s1.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.f2.$ contains x.s1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.f2.!t1.$ contains x.s1.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.f2.!t2.$ contains x.s1.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.[el].* contains x.s1.* : containsAccess(abstract-prefix wildcard) + x.s1.[el].* contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.s1.[el].* contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.[el].* contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.[el].* contains x.s1.[el].$ : containsAccess(abstract-prefix wildcard) + x.s1.[el].* contains x.s1.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.[el].* contains x.s1.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.[el].$ contains x.s1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.[el].!t1.$ contains x.s1.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s1.[el].!t2.$ contains x.s1.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.* contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f1.* : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f1.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f2.* : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.[el].* : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.[el].$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.* contains x.s2.*f : containsAccess(abstract-prefix wildcard) + x.s2.$ contains x.s2.f1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.$ contains x.s2.f2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.$ contains x.s2.[el].$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.!t1.$ contains x.s2.f1.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.!t1.$ contains x.s2.f2.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.!t1.$ contains x.s2.[el].!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.!t2.$ contains x.s2.f1.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.!t2.$ contains x.s2.f2.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.!t2.$ contains x.s2.[el].!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.f1.* contains x.s2.* : containsAccess(abstract-prefix wildcard) + x.s2.f1.* contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.s2.f1.* contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.f1.* contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.f1.* contains x.s2.f1.$ : containsAccess(abstract-prefix wildcard) + x.s2.f1.* contains x.s2.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.f1.* contains x.s2.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.f1.$ contains x.s2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.f1.!t1.$ contains x.s2.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.f1.!t2.$ contains x.s2.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.f2.* contains x.s2.* : containsAccess(abstract-prefix wildcard) + x.s2.f2.* contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.s2.f2.* contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.f2.* contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.f2.* contains x.s2.f2.$ : containsAccess(abstract-prefix wildcard) + x.s2.f2.* contains x.s2.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.f2.* contains x.s2.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.f2.$ contains x.s2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.f2.!t1.$ contains x.s2.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.f2.!t2.$ contains x.s2.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.[el].* contains x.s2.* : containsAccess(abstract-prefix wildcard) + x.s2.[el].* contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.s2.[el].* contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.[el].* contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.[el].* contains x.s2.[el].$ : containsAccess(abstract-prefix wildcard) + x.s2.[el].* contains x.s2.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.[el].* contains x.s2.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.[el].$ contains x.s2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.[el].!t1.$ contains x.s2.!t1.$ : covers(directional virtual field-[any]; suffix+static exact) + x.s2.[el].!t2.$ contains x.s2.!t2.$ : covers(directional virtual field-[any]; suffix+static exact) + x.*s contains x.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.f1.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.f1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.f2.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.f2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.[el].* : containsAccess(abstract-prefix wildcard) + x.*s contains x.[el].$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f1.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f2.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.[el].* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.[el].$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f1.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f2.* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.[el].* : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.[el].$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.*s contains x.*f : containsAccess(abstract-prefix wildcard) + x.*s contains x.s1.*f : containsAccess(abstract-prefix wildcard) + x.*s contains x.s2.*f : containsAccess(abstract-prefix wildcard) + x.*f contains x.* : containsAccess(abstract-prefix wildcard) + x.*f contains x.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.f1.* : containsAccess(abstract-prefix wildcard) + x.*f contains x.f1.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.f2.* : containsAccess(abstract-prefix wildcard) + x.*f contains x.f2.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.[el].* : containsAccess(abstract-prefix wildcard) + x.*f contains x.[el].$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.*f contains x.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.* : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f1.* : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f2.* : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f2.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.[el].* : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.[el].$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.s1.*f contains x.s1.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.* : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f1.* : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f1.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f1.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f1.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f2.* : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f2.!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.f2.!t2.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.[el].* : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.[el].$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.[el].!t1.$ : containsAccess(abstract-prefix wildcard) + x.s2.*f contains x.s2.[el].!t2.$ : containsAccess(abstract-prefix wildcard) + +## CROSS-BASE PROBE x-fact.contains(y-same-access) + cross-base identical-access contained count = 0 / 52 diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode0.golden.txt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode0.golden.txt new file mode 100644 index 000000000..682f687cf --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode0.golden.txt @@ -0,0 +1,65 @@ +================================================================ +BASE-ONLY delta/concat PIN — mode fieldSensitive=false +slots=(static,field,suffix) suffix: -2=abstract(*) 3=value($) >=0-other=mark (empty is not a fact) +================================================================ + +## FACTS (13) + F00 = x.* (-1,-1,-2) [ap@2] + F01 = x.!t1.$ (-1,-1, 2) [mark] + F02 = x.!t2.$ (-1,-1, 6) [mark] + F03 = x.s1.* ( 1,-1,-2) [ap@2] + F04 = x.s1.!t1.$ ( 1,-1, 2) [mark] + F05 = x.s1.!t2.$ ( 1,-1, 6) [mark] + F06 = x.s2.* ( 5,-1,-2) [ap@2] + F07 = x.s2.!t1.$ ( 5,-1, 2) [mark] + F08 = x.s2.!t2.$ ( 5,-1, 6) [mark] + F09 = x.*s (-2,-1,-1) [ap@0] + F10 = x.*f (-1,-2,-1) [ap@1] + F11 = x.s1.*f ( 1,-2,-1) [ap@1] + F12 = x.s2.*f ( 5,-2,-1) [ap@1] + +## DISTINCT DELTAS (11) [from all 13x13 ordered pairs final.delta(initial)] + D00 = ε + D01 = Δ.!t1.$ + D02 = Δ.!t2.$ + D03 = Δ.s1.* + D04 = Δ.s1.!t1.$ + D05 = Δ.s1.!t2.$ + D06 = Δ.s2.* + D07 = Δ.s2.!t1.$ + D08 = Δ.s2.!t2.$ + D09 = Δ.s1.*f + D10 = Δ.s2.*f + ('-' in the matrix below = NO-MATCH, empty delta list) + +## DELTA MATRIX cell = F_row.delta(F_col) + | F00 | F01 | F02 | F03 | F04 | F05 | F06 | F07 | F08 | F09 | F10 | F11 | F12 + F00 | D0 | - | - | - | - | - | - | - | - | - | - | - | - + F01 | D1 | D0 | - | - | - | - | - | - | - | - | - | - | - + F02 | D2 | - | D0 | - | - | - | - | - | - | - | - | - | - + F03 | - | - | - | D0 | - | - | - | - | - | D3 | - | - | - + F04 | - | - | - | D1 | D0 | - | - | - | - | D4 | - | - | - + F05 | - | - | - | D2 | - | D0 | - | - | - | D5 | - | - | - + F06 | - | - | - | - | - | - | D0 | - | - | D6 | - | - | - + F07 | - | - | - | - | - | - | D1 | D0 | - | D7 | - | - | - + F08 | - | - | - | - | - | - | D2 | - | D0 | D8 | - | - | - + F09 | - | - | - | - | - | - | - | - | - | D0 | - | - | - + F10 | - | - | - | - | - | - | - | - | - | - | D0 | - | - + F11 | - | - | - | - | - | - | - | - | - | D9 | - | D0 | - + F12 | - | - | - | - | - | - | - | - | - | D10 | - | - | D0 + +## CONCAT MATRIX cell = F_row.concat(D_col) + | D00 | D01 | D02 | D03 | D04 | D05 | D06 | D07 | D08 | D09 | D10 + F00 | x.* | x.!t1.$ | x.!t2.$ | null | null | null | null | null | null | null | null + F01 | x.!t1.$ | null | null | null | null | null | null | null | null | null | null + F02 | x.!t2.$ | null | null | null | null | null | null | null | null | null | null + F03 | x.s1.* | x.s1.!t1.$ | x.s1.!t2.$ | null | null | null | null | null | null | null | null + F04 | x.s1.!t1.$ | null | null | null | null | null | null | null | null | null | null + F05 | x.s1.!t2.$ | null | null | null | null | null | null | null | null | null | null + F06 | x.s2.* | x.s2.!t1.$ | x.s2.!t2.$ | null | null | null | null | null | null | null | null + F07 | x.s2.!t1.$ | null | null | null | null | null | null | null | null | null | null + F08 | x.s2.!t2.$ | null | null | null | null | null | null | null | null | null | null + F09 | x.*s | x.!t1.$ | x.!t2.$ | x.s1.* | x.s1.!t1.$ | x.s1.!t2.$ | x.s2.* | x.s2.!t1.$ | x.s2.!t2.$ | x.s1.*f | x.s2.*f + F10 | x.*f | x.!t1.$ | x.!t2.$ | null | null | null | null | null | null | null | null + F11 | x.s1.*f | x.s1.!t1.$ | x.s1.!t2.$ | null | null | null | null | null | null | null | null + F12 | x.s2.*f | x.s2.!t1.$ | x.s2.!t2.$ | null | null | null | null | null | null | null | null diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode1.golden.txt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode1.golden.txt new file mode 100644 index 000000000..f5b037d70 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode1.golden.txt @@ -0,0 +1,138 @@ +================================================================ +BASE-ONLY delta/concat PIN — mode fieldSensitive=true +slots=(static,field,suffix) suffix: -2=abstract(*) 3=value($) >=0-other=mark (empty is not a fact) +================================================================ + +## FACTS (31) + F00 = x.* (-1,-1,-2) [ap@2] + F01 = x.!t1.$ (-1,-1, 2) [mark] + F02 = x.!t2.$ (-1,-1, 6) [mark] + F03 = x.f1.* (-1, 0,-2) [ap@2] + F04 = x.f1.!t1.$ (-1, 0, 2) [mark] + F05 = x.f1.!t2.$ (-1, 0, 6) [mark] + F06 = x.f2.* (-1, 4,-2) [ap@2] + F07 = x.f2.!t1.$ (-1, 4, 2) [mark] + F08 = x.f2.!t2.$ (-1, 4, 6) [mark] + F09 = x.s1.* ( 1,-1,-2) [ap@2] + F10 = x.s1.!t1.$ ( 1,-1, 2) [mark] + F11 = x.s1.!t2.$ ( 1,-1, 6) [mark] + F12 = x.s1.f1.* ( 1, 0,-2) [ap@2] + F13 = x.s1.f1.!t1.$ ( 1, 0, 2) [mark] + F14 = x.s1.f1.!t2.$ ( 1, 0, 6) [mark] + F15 = x.s1.f2.* ( 1, 4,-2) [ap@2] + F16 = x.s1.f2.!t1.$ ( 1, 4, 2) [mark] + F17 = x.s1.f2.!t2.$ ( 1, 4, 6) [mark] + F18 = x.s2.* ( 5,-1,-2) [ap@2] + F19 = x.s2.!t1.$ ( 5,-1, 2) [mark] + F20 = x.s2.!t2.$ ( 5,-1, 6) [mark] + F21 = x.s2.f1.* ( 5, 0,-2) [ap@2] + F22 = x.s2.f1.!t1.$ ( 5, 0, 2) [mark] + F23 = x.s2.f1.!t2.$ ( 5, 0, 6) [mark] + F24 = x.s2.f2.* ( 5, 4,-2) [ap@2] + F25 = x.s2.f2.!t1.$ ( 5, 4, 2) [mark] + F26 = x.s2.f2.!t2.$ ( 5, 4, 6) [mark] + F27 = x.*s (-2,-1,-1) [ap@0] + F28 = x.*f (-1,-2,-1) [ap@1] + F29 = x.s1.*f ( 1,-2,-1) [ap@1] + F30 = x.s2.*f ( 5,-2,-1) [ap@1] + +## DISTINCT DELTAS (30) [from all 31x31 ordered pairs final.delta(initial)] + D00 = ε + D01 = Δ.!t1.$ + D02 = Δ.!t2.$ + D03 = Δ.* + D04 = Δ.f1.* + D05 = Δ.f1.!t1.$ + D06 = Δ.f1.!t2.$ + D07 = Δ.f2.* + D08 = Δ.f2.!t1.$ + D09 = Δ.f2.!t2.$ + D10 = Δ.s1.* + D11 = Δ.s1.!t1.$ + D12 = Δ.s1.!t2.$ + D13 = Δ.s1.f1.* + D14 = Δ.s1.f1.!t1.$ + D15 = Δ.s1.f1.!t2.$ + D16 = Δ.s1.f2.* + D17 = Δ.s1.f2.!t1.$ + D18 = Δ.s1.f2.!t2.$ + D19 = Δ.s2.* + D20 = Δ.s2.!t1.$ + D21 = Δ.s2.!t2.$ + D22 = Δ.s2.f1.* + D23 = Δ.s2.f1.!t1.$ + D24 = Δ.s2.f1.!t2.$ + D25 = Δ.s2.f2.* + D26 = Δ.s2.f2.!t1.$ + D27 = Δ.s2.f2.!t2.$ + D28 = Δ.s1.*f + D29 = Δ.s2.*f + ('-' in the matrix below = NO-MATCH, empty delta list) + +## DELTA MATRIX cell = F_row.delta(F_col) + | F00 | F01 | F02 | F03 | F04 | F05 | F06 | F07 | F08 | F09 | F10 | F11 | F12 | F13 | F14 | F15 | F16 | F17 | F18 | F19 | F20 | F21 | F22 | F23 | F24 | F25 | F26 | F27 | F28 | F29 | F30 + F00 | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - + F01 | D1 | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - + F02 | D2 | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - + F03 | D3 | - | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D4 | - | - + F04 | D1 | - | - | D1 | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D5 | - | - + F05 | D2 | - | - | D2 | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D6 | - | - + F06 | D3 | - | - | - | - | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D7 | - | - + F07 | D1 | - | - | - | - | - | D1 | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D8 | - | - + F08 | D2 | - | - | - | - | - | D2 | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D9 | - | - + F09 | - | - | - | - | - | - | - | - | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D10 | - | - | - + F10 | - | - | - | - | - | - | - | - | - | D1 | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D11 | - | - | - + F11 | - | - | - | - | - | - | - | - | - | D2 | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D12 | - | - | - + F12 | - | - | - | - | - | - | - | - | - | D3 | - | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D13 | - | D4 | - + F13 | - | - | - | - | - | - | - | - | - | D1 | - | - | D1 | D0 | - | - | - | - | - | - | - | - | - | - | - | - | - | D14 | - | D5 | - + F14 | - | - | - | - | - | - | - | - | - | D2 | - | - | D2 | - | D0 | - | - | - | - | - | - | - | - | - | - | - | - | D15 | - | D6 | - + F15 | - | - | - | - | - | - | - | - | - | D3 | - | - | - | - | - | D0 | - | - | - | - | - | - | - | - | - | - | - | D16 | - | D7 | - + F16 | - | - | - | - | - | - | - | - | - | D1 | - | - | - | - | - | D1 | D0 | - | - | - | - | - | - | - | - | - | - | D17 | - | D8 | - + F17 | - | - | - | - | - | - | - | - | - | D2 | - | - | - | - | - | D2 | - | D0 | - | - | - | - | - | - | - | - | - | D18 | - | D9 | - + F18 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D0 | - | - | - | - | - | - | - | - | D19 | - | - | - + F19 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D1 | D0 | - | - | - | - | - | - | - | D20 | - | - | - + F20 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D2 | - | D0 | - | - | - | - | - | - | D21 | - | - | - + F21 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D3 | - | - | D0 | - | - | - | - | - | D22 | - | - | D4 + F22 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D1 | - | - | D1 | D0 | - | - | - | - | D23 | - | - | D5 + F23 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D2 | - | - | D2 | - | D0 | - | - | - | D24 | - | - | D6 + F24 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D3 | - | - | - | - | - | D0 | - | - | D25 | - | - | D7 + F25 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D1 | - | - | - | - | - | D1 | D0 | - | D26 | - | - | D8 + F26 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D2 | - | - | - | - | - | D2 | - | D0 | D27 | - | - | D9 + F27 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D0 | - | - | - + F28 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D0 | - | - + F29 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D28 | - | D0 | - + F30 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | D29 | - | - | D0 + +## CONCAT MATRIX cell = F_row.concat(D_col) + | D00 | D01 | D02 | D03 | D04 | D05 | D06 | D07 | D08 | D09 | D10 | D11 | D12 | D13 | D14 | D15 | D16 | D17 | D18 | D19 | D20 | D21 | D22 | D23 | D24 | D25 | D26 | D27 | D28 | D29 + F00 | x.* | x.!t1.$ | x.!t2.$ | x.* | x.* | x.!t1.$ | x.!t2.$ | x.* | x.!t1.$ | x.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F01 | x.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F02 | x.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F03 | x.f1.* | x.f1.!t1.$ | x.f1.!t2.$ | x.f1.* | x.f1.* | x.f1.!t1.$ | x.f1.!t2.$ | x.f1.* | x.f1.!t1.$ | x.f1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F04 | x.f1.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F05 | x.f1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F06 | x.f2.* | x.f2.!t1.$ | x.f2.!t2.$ | x.f2.* | x.f2.* | x.f2.!t1.$ | x.f2.!t2.$ | x.f2.* | x.f2.!t1.$ | x.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F07 | x.f2.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F08 | x.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F09 | x.s1.* | x.s1.!t1.$ | x.s1.!t2.$ | x.s1.* | x.s1.* | x.s1.!t1.$ | x.s1.!t2.$ | x.s1.* | x.s1.!t1.$ | x.s1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F10 | x.s1.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F11 | x.s1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F12 | x.s1.f1.* | x.s1.f1.!t1.$ | x.s1.f1.!t2.$ | x.s1.f1.* | x.s1.f1.* | x.s1.f1.!t1.$ | x.s1.f1.!t2.$ | x.s1.f1.* | x.s1.f1.!t1.$ | x.s1.f1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F13 | x.s1.f1.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F14 | x.s1.f1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F15 | x.s1.f2.* | x.s1.f2.!t1.$ | x.s1.f2.!t2.$ | x.s1.f2.* | x.s1.f2.* | x.s1.f2.!t1.$ | x.s1.f2.!t2.$ | x.s1.f2.* | x.s1.f2.!t1.$ | x.s1.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F16 | x.s1.f2.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F17 | x.s1.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F18 | x.s2.* | x.s2.!t1.$ | x.s2.!t2.$ | x.s2.* | x.s2.* | x.s2.!t1.$ | x.s2.!t2.$ | x.s2.* | x.s2.!t1.$ | x.s2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F19 | x.s2.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F20 | x.s2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F21 | x.s2.f1.* | x.s2.f1.!t1.$ | x.s2.f1.!t2.$ | x.s2.f1.* | x.s2.f1.* | x.s2.f1.!t1.$ | x.s2.f1.!t2.$ | x.s2.f1.* | x.s2.f1.!t1.$ | x.s2.f1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F22 | x.s2.f1.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F23 | x.s2.f1.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F24 | x.s2.f2.* | x.s2.f2.!t1.$ | x.s2.f2.!t2.$ | x.s2.f2.* | x.s2.f2.* | x.s2.f2.!t1.$ | x.s2.f2.!t2.$ | x.s2.f2.* | x.s2.f2.!t1.$ | x.s2.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F25 | x.s2.f2.!t1.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F26 | x.s2.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F27 | x.*s | x.!t1.$ | x.!t2.$ | x.* | x.f1.* | x.f1.!t1.$ | x.f1.!t2.$ | x.f2.* | x.f2.!t1.$ | x.f2.!t2.$ | x.s1.* | x.s1.!t1.$ | x.s1.!t2.$ | x.s1.f1.* | x.s1.f1.!t1.$ | x.s1.f1.!t2.$ | x.s1.f2.* | x.s1.f2.!t1.$ | x.s1.f2.!t2.$ | x.s2.* | x.s2.!t1.$ | x.s2.!t2.$ | x.s2.f1.* | x.s2.f1.!t1.$ | x.s2.f1.!t2.$ | x.s2.f2.* | x.s2.f2.!t1.$ | x.s2.f2.!t2.$ | x.s1.*f | x.s2.*f + F28 | x.*f | x.!t1.$ | x.!t2.$ | x.* | x.f1.* | x.f1.!t1.$ | x.f1.!t2.$ | x.f2.* | x.f2.!t1.$ | x.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F29 | x.s1.*f | x.s1.!t1.$ | x.s1.!t2.$ | x.s1.* | x.s1.f1.* | x.s1.f1.!t1.$ | x.s1.f1.!t2.$ | x.s1.f2.* | x.s1.f2.!t1.$ | x.s1.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null + F30 | x.s2.*f | x.s2.!t1.$ | x.s2.!t2.$ | x.s2.* | x.s2.f1.* | x.s2.f1.!t1.$ | x.s2.f1.!t2.$ | x.s2.f2.* | x.s2.f2.!t1.$ | x.s2.f2.!t2.$ | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode0.golden.txt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode0.golden.txt new file mode 100644 index 000000000..b1915905a --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode0.golden.txt @@ -0,0 +1,92 @@ +================================================================ +BASE-ONLY split-delta vs contains ALIGNMENT PIN — fieldSensitive=false +cell (final=row, initial=col): + . not contained, no match : not contained, structural residual + e contained, ε residual (aligned) d contained, structural Δ residual (matched) + X contained but DROPPED (misalign) S not contained but ε (over-match) +Alignment invariant: no X, no S. +================================================================ + +## FACTS (16) + F00 = x.* (-1,-1,-2) [ap@2] + F01 = x.$ (-1,-1, 3) [value] + F02 = x.!t1.$ (-1,-1, 2) [mark] + F03 = x.!t2.$ (-1,-1, 6) [mark] + F04 = x.s1.* ( 1,-1,-2) [ap@2] + F05 = x.s1.$ ( 1,-1, 3) [value] + F06 = x.s1.!t1.$ ( 1,-1, 2) [mark] + F07 = x.s1.!t2.$ ( 1,-1, 6) [mark] + F08 = x.s2.* ( 5,-1,-2) [ap@2] + F09 = x.s2.$ ( 5,-1, 3) [value] + F10 = x.s2.!t1.$ ( 5,-1, 2) [mark] + F11 = x.s2.!t2.$ ( 5,-1, 6) [mark] + F12 = x.*s (-2,-1,-1) [ap@0] + F13 = x.*f (-1,-2,-1) [ap@1] + F14 = x.s1.*f ( 1,-2,-1) [ap@1] + F15 = x.s2.*f ( 5,-2,-1) [ap@1] + +## ALIGNMENT MATRIX + F00 F01 F02 F03 F04 F05 F06 F07 F08 F09 F10 F11 F12 F13 F14 F15 + F00 e d d d . . . . . . . . . e . . + F01 . e . . . . . . . . . . . . . . + F02 . . e . . . . . . . . . . . . . + F03 . . . e . . . . . . . . . . . . + F04 . . . . e d d d . . . . . . e . + F05 . . . . . e . . . . . . . . . . + F06 . . . . . . e . . . . . . . . . + F07 . . . . . . . e . . . . . . . . + F08 . . . . . . . . e d d d . . . e + F09 . . . . . . . . . e . . . . . . + F10 . . . . . . . . . . e . . . . . + F11 . . . . . . . . . . . e . . . . + F12 e d d d e d d d e d d d e e e e + F13 d d d d . . . . . . . . . e . . + F14 . . . . d d d d . . . . . . e . + F15 . . . . . . . . d d d d . . . e + +## SUMMARY (symbol counts) + 'e' : 25 + 'd' : 30 + '.' : 201 + +## CONTAINMENT PAIRS (contained, off-diagonal) — residual per pair + final initial | sym | splitDelta(i,f) + x.* x.$ | d | [m.*]Δ.$ + x.* x.!t1.$ | d | [m.*]Δ.!t1.$ + x.* x.!t2.$ | d | [m.*]Δ.!t2.$ + x.* x.*f | e | [m.*]ε + x.s1.* x.s1.$ | d | [m.s1.*]Δ.$ + x.s1.* x.s1.!t1.$ | d | [m.s1.*]Δ.!t1.$ + x.s1.* x.s1.!t2.$ | d | [m.s1.*]Δ.!t2.$ + x.s1.* x.s1.*f | e | [m.s1.*]ε + x.s2.* x.s2.$ | d | [m.s2.*]Δ.$ + x.s2.* x.s2.!t1.$ | d | [m.s2.*]Δ.!t1.$ + x.s2.* x.s2.!t2.$ | d | [m.s2.*]Δ.!t2.$ + x.s2.* x.s2.*f | e | [m.s2.*]ε + x.*s x.* | e | [m.*s]ε + x.*s x.$ | d | [m.*s]Δ.$ + x.*s x.!t1.$ | d | [m.*s]Δ.!t1.$ + x.*s x.!t2.$ | d | [m.*s]Δ.!t2.$ + x.*s x.s1.* | e | [m.*s]ε + x.*s x.s1.$ | d | [m.*s]Δ.s1.$ + x.*s x.s1.!t1.$ | d | [m.*s]Δ.s1.!t1.$ + x.*s x.s1.!t2.$ | d | [m.*s]Δ.s1.!t2.$ + x.*s x.s2.* | e | [m.*s]ε + x.*s x.s2.$ | d | [m.*s]Δ.s2.$ + x.*s x.s2.!t1.$ | d | [m.*s]Δ.s2.!t1.$ + x.*s x.s2.!t2.$ | d | [m.*s]Δ.s2.!t2.$ + x.*s x.*f | e | [m.*s]ε + x.*s x.s1.*f | e | [m.*s]ε + x.*s x.s2.*f | e | [m.*s]ε + x.*f x.* | d | [m.*f]Δ.* + x.*f x.$ | d | [m.*f]Δ.$ + x.*f x.!t1.$ | d | [m.*f]Δ.!t1.$ + x.*f x.!t2.$ | d | [m.*f]Δ.!t2.$ + x.s1.*f x.s1.* | d | [m.s1.*f]Δ.* + x.s1.*f x.s1.$ | d | [m.s1.*f]Δ.$ + x.s1.*f x.s1.!t1.$ | d | [m.s1.*f]Δ.!t1.$ + x.s1.*f x.s1.!t2.$ | d | [m.s1.*f]Δ.!t2.$ + x.s2.*f x.s2.* | d | [m.s2.*f]Δ.* + x.s2.*f x.s2.$ | d | [m.s2.*f]Δ.$ + x.s2.*f x.s2.!t1.$ | d | [m.s2.*f]Δ.!t1.$ + x.s2.*f x.s2.!t2.$ | d | [m.s2.*f]Δ.!t2.$ diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode1.golden.txt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode1.golden.txt new file mode 100644 index 000000000..b17e9da83 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode1.golden.txt @@ -0,0 +1,389 @@ +================================================================ +BASE-ONLY split-delta vs contains ALIGNMENT PIN — fieldSensitive=true +cell (final=row, initial=col): + . not contained, no match : not contained, structural residual + e contained, ε residual (aligned) d contained, structural Δ residual (matched) + X contained but DROPPED (misalign) S not contained but ε (over-match) +Alignment invariant: no X, no S. +================================================================ + +## FACTS (52) + F00 = x.* (-1,-1,-2) [ap@2] + F01 = x.$ (-1,-1, 3) [value] + F02 = x.!t1.$ (-1,-1, 2) [mark] + F03 = x.!t2.$ (-1,-1, 6) [mark] + F04 = x.f1.* (-1, 0,-2) [ap@2] + F05 = x.f1.$ (-1, 0, 3) [value] + F06 = x.f1.!t1.$ (-1, 0, 2) [mark] + F07 = x.f1.!t2.$ (-1, 0, 6) [mark] + F08 = x.f2.* (-1, 4,-2) [ap@2] + F09 = x.f2.$ (-1, 4, 3) [value] + F10 = x.f2.!t1.$ (-1, 4, 2) [mark] + F11 = x.f2.!t2.$ (-1, 4, 6) [mark] + F12 = x.[el].* (-1,11,-2) [ap@2] + F13 = x.[el].$ (-1,11, 3) [value] + F14 = x.[el].!t1.$ (-1,11, 2) [mark] + F15 = x.[el].!t2.$ (-1,11, 6) [mark] + F16 = x.s1.* ( 1,-1,-2) [ap@2] + F17 = x.s1.$ ( 1,-1, 3) [value] + F18 = x.s1.!t1.$ ( 1,-1, 2) [mark] + F19 = x.s1.!t2.$ ( 1,-1, 6) [mark] + F20 = x.s1.f1.* ( 1, 0,-2) [ap@2] + F21 = x.s1.f1.$ ( 1, 0, 3) [value] + F22 = x.s1.f1.!t1.$ ( 1, 0, 2) [mark] + F23 = x.s1.f1.!t2.$ ( 1, 0, 6) [mark] + F24 = x.s1.f2.* ( 1, 4,-2) [ap@2] + F25 = x.s1.f2.$ ( 1, 4, 3) [value] + F26 = x.s1.f2.!t1.$ ( 1, 4, 2) [mark] + F27 = x.s1.f2.!t2.$ ( 1, 4, 6) [mark] + F28 = x.s1.[el].* ( 1,11,-2) [ap@2] + F29 = x.s1.[el].$ ( 1,11, 3) [value] + F30 = x.s1.[el].!t1.$ ( 1,11, 2) [mark] + F31 = x.s1.[el].!t2.$ ( 1,11, 6) [mark] + F32 = x.s2.* ( 5,-1,-2) [ap@2] + F33 = x.s2.$ ( 5,-1, 3) [value] + F34 = x.s2.!t1.$ ( 5,-1, 2) [mark] + F35 = x.s2.!t2.$ ( 5,-1, 6) [mark] + F36 = x.s2.f1.* ( 5, 0,-2) [ap@2] + F37 = x.s2.f1.$ ( 5, 0, 3) [value] + F38 = x.s2.f1.!t1.$ ( 5, 0, 2) [mark] + F39 = x.s2.f1.!t2.$ ( 5, 0, 6) [mark] + F40 = x.s2.f2.* ( 5, 4,-2) [ap@2] + F41 = x.s2.f2.$ ( 5, 4, 3) [value] + F42 = x.s2.f2.!t1.$ ( 5, 4, 2) [mark] + F43 = x.s2.f2.!t2.$ ( 5, 4, 6) [mark] + F44 = x.s2.[el].* ( 5,11,-2) [ap@2] + F45 = x.s2.[el].$ ( 5,11, 3) [value] + F46 = x.s2.[el].!t1.$ ( 5,11, 2) [mark] + F47 = x.s2.[el].!t2.$ ( 5,11, 6) [mark] + F48 = x.*s (-2,-1,-1) [ap@0] + F49 = x.*f (-1,-2,-1) [ap@1] + F50 = x.s1.*f ( 1,-2,-1) [ap@1] + F51 = x.s2.*f ( 5,-2,-1) [ap@1] + +## ALIGNMENT MATRIX + F00 F01 F02 F03 F04 F05 F06 F07 F08 F09 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31 F32 F33 F34 F35 F36 F37 F38 F39 F40 F41 F42 F43 F44 F45 F46 F47 F48 F49 F50 F51 + F00 e d d d e d d d e d d d e d d d . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . + F01 . e . . . e . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F02 . . e . . . e . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F03 . . . e . . . e . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F04 e d d d e d d d . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F05 . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F06 . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F07 . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F08 e d d d . . . . e d d d . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F09 . e . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F10 . . e . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F11 . . . e . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F12 e d d d . . . . . . . . e d d d . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F13 . e . . . . . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F14 . . e . . . . . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F15 . . . e . . . . . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F16 . . . . . . . . . . . . . . . . e d d d e d d d e d d d e d d d . . . . . . . . . . . . . . . . . . e . + F17 . . . . . . . . . . . . . . . . . e . . . e . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . + F18 . . . . . . . . . . . . . . . . . . e . . . e . . . e . . . e . . . . . . . . . . . . . . . . . . . . . + F19 . . . . . . . . . . . . . . . . . . . e . . . e . . . e . . . e . . . . . . . . . . . . . . . . . . . . + F20 . . . . . . . . . . . . . . . . e d d d e d d d . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F21 . . . . . . . . . . . . . . . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F22 . . . . . . . . . . . . . . . . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F23 . . . . . . . . . . . . . . . . . . . e . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . . . + F24 . . . . . . . . . . . . . . . . e d d d . . . . e d d d . . . . . . . . . . . . . . . . . . . . . . . . + F25 . . . . . . . . . . . . . . . . . e . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . . + F26 . . . . . . . . . . . . . . . . . . e . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . . + F27 . . . . . . . . . . . . . . . . . . . e . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . . . + F28 . . . . . . . . . . . . . . . . e d d d . . . . . . . . e d d d . . . . . . . . . . . . . . . . . . . . + F29 . . . . . . . . . . . . . . . . . e . . . . . . . . . . . e . . . . . . . . . . . . . . . . . . . . . . + F30 . . . . . . . . . . . . . . . . . . e . . . . . . . . . . . e . . . . . . . . . . . . . . . . . . . . . + F31 . . . . . . . . . . . . . . . . . . . e . . . . . . . . . . . e . . . . . . . . . . . . . . . . . . . . + F32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e d d d e d d d e d d d e d d d . . . e + F33 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . e . . . e . . . e . . . . . . + F34 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . e . . . e . . . e . . . . . + F35 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . e . . . e . . . e . . . . + F36 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e d d d e d d d . . . . . . . . . . . . + F37 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . e . . . . . . . . . . . . . . + F38 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . e . . . . . . . . . . . . . + F39 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . e . . . . . . . . . . . . + F40 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e d d d . . . . e d d d . . . . . . . . + F41 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . . . . . e . . . . . . . . . . + F42 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . . . . . e . . . . . . . . . + F43 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . . . . . e . . . . . . . . + F44 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e d d d . . . . . . . . e d d d . . . . + F45 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . . . . . . . . . e . . . . . . + F46 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . . . . . . . . . e . . . . . + F47 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . . . . . . . . . . e . . . . + F48 e d d d e d d d e d d d e d d d e d d d e d d d e d d d e d d d e d d d e d d d e d d d e d d d e e e e + F49 d d d d d d d d d d d d d d d d . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . e . . + F50 . . . . . . . . . . . . . . . . d d d d d d d d d d d d d d d d . . . . . . . . . . . . . . . . . . e . + F51 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . d d d d d d d d d d d d d d d d . . . e + +## SUMMARY (symbol counts) + 'e' : 142 + 'd' : 174 + '.' : 2388 + +## CONTAINMENT PAIRS (contained, off-diagonal) — residual per pair + final initial | sym | splitDelta(i,f) + x.* x.$ | d | [m.*]Δ.$ + x.* x.!t1.$ | d | [m.*]Δ.!t1.$ + x.* x.!t2.$ | d | [m.*]Δ.!t2.$ + x.* x.f1.* | e | [m.*]ε + x.* x.f1.$ | d | [m.*]Δ.$ + x.* x.f1.!t1.$ | d | [m.*]Δ.!t1.$ + x.* x.f1.!t2.$ | d | [m.*]Δ.!t2.$ + x.* x.f2.* | e | [m.*]ε + x.* x.f2.$ | d | [m.*]Δ.$ + x.* x.f2.!t1.$ | d | [m.*]Δ.!t1.$ + x.* x.f2.!t2.$ | d | [m.*]Δ.!t2.$ + x.* x.[el].* | e | [m.*]ε + x.* x.[el].$ | d | [m.*]Δ.$ + x.* x.[el].!t1.$ | d | [m.*]Δ.!t1.$ + x.* x.[el].!t2.$ | d | [m.*]Δ.!t2.$ + x.* x.*f | e | [m.*]ε + x.$ x.f1.$ | e | [m.$]ε + x.$ x.f2.$ | e | [m.$]ε + x.$ x.[el].$ | e | [m.$]ε + x.!t1.$ x.f1.!t1.$ | e | [m.!t1.$]ε + x.!t1.$ x.f2.!t1.$ | e | [m.!t1.$]ε + x.!t1.$ x.[el].!t1.$ | e | [m.!t1.$]ε + x.!t2.$ x.f1.!t2.$ | e | [m.!t2.$]ε + x.!t2.$ x.f2.!t2.$ | e | [m.!t2.$]ε + x.!t2.$ x.[el].!t2.$ | e | [m.!t2.$]ε + x.f1.* x.* | e | [m.f1.*]ε + x.f1.* x.$ | d | [m.f1.*]Δ.$ + x.f1.* x.!t1.$ | d | [m.f1.*]Δ.!t1.$ + x.f1.* x.!t2.$ | d | [m.f1.*]Δ.!t2.$ + x.f1.* x.f1.$ | d | [m.f1.*]Δ.$ + x.f1.* x.f1.!t1.$ | d | [m.f1.*]Δ.!t1.$ + x.f1.* x.f1.!t2.$ | d | [m.f1.*]Δ.!t2.$ + x.f1.$ x.$ | e | [m.f1.$]ε + x.f1.!t1.$ x.!t1.$ | e | [m.f1.!t1.$]ε + x.f1.!t2.$ x.!t2.$ | e | [m.f1.!t2.$]ε + x.f2.* x.* | e | [m.f2.*]ε + x.f2.* x.$ | d | [m.f2.*]Δ.$ + x.f2.* x.!t1.$ | d | [m.f2.*]Δ.!t1.$ + x.f2.* x.!t2.$ | d | [m.f2.*]Δ.!t2.$ + x.f2.* x.f2.$ | d | [m.f2.*]Δ.$ + x.f2.* x.f2.!t1.$ | d | [m.f2.*]Δ.!t1.$ + x.f2.* x.f2.!t2.$ | d | [m.f2.*]Δ.!t2.$ + x.f2.$ x.$ | e | [m.f2.$]ε + x.f2.!t1.$ x.!t1.$ | e | [m.f2.!t1.$]ε + x.f2.!t2.$ x.!t2.$ | e | [m.f2.!t2.$]ε + x.[el].* x.* | e | [m.[el].*]ε + x.[el].* x.$ | d | [m.[el].*]Δ.$ + x.[el].* x.!t1.$ | d | [m.[el].*]Δ.!t1.$ + x.[el].* x.!t2.$ | d | [m.[el].*]Δ.!t2.$ + x.[el].* x.[el].$ | d | [m.[el].*]Δ.$ + x.[el].* x.[el].!t1.$ | d | [m.[el].*]Δ.!t1.$ + x.[el].* x.[el].!t2.$ | d | [m.[el].*]Δ.!t2.$ + x.[el].$ x.$ | e | [m.[el].$]ε + x.[el].!t1.$ x.!t1.$ | e | [m.[el].!t1.$]ε + x.[el].!t2.$ x.!t2.$ | e | [m.[el].!t2.$]ε + x.s1.* x.s1.$ | d | [m.s1.*]Δ.$ + x.s1.* x.s1.!t1.$ | d | [m.s1.*]Δ.!t1.$ + x.s1.* x.s1.!t2.$ | d | [m.s1.*]Δ.!t2.$ + x.s1.* x.s1.f1.* | e | [m.s1.*]ε + x.s1.* x.s1.f1.$ | d | [m.s1.*]Δ.$ + x.s1.* x.s1.f1.!t1.$ | d | [m.s1.*]Δ.!t1.$ + x.s1.* x.s1.f1.!t2.$ | d | [m.s1.*]Δ.!t2.$ + x.s1.* x.s1.f2.* | e | [m.s1.*]ε + x.s1.* x.s1.f2.$ | d | [m.s1.*]Δ.$ + x.s1.* x.s1.f2.!t1.$ | d | [m.s1.*]Δ.!t1.$ + x.s1.* x.s1.f2.!t2.$ | d | [m.s1.*]Δ.!t2.$ + x.s1.* x.s1.[el].* | e | [m.s1.*]ε + x.s1.* x.s1.[el].$ | d | [m.s1.*]Δ.$ + x.s1.* x.s1.[el].!t1.$ | d | [m.s1.*]Δ.!t1.$ + x.s1.* x.s1.[el].!t2.$ | d | [m.s1.*]Δ.!t2.$ + x.s1.* x.s1.*f | e | [m.s1.*]ε + x.s1.$ x.s1.f1.$ | e | [m.s1.$]ε + x.s1.$ x.s1.f2.$ | e | [m.s1.$]ε + x.s1.$ x.s1.[el].$ | e | [m.s1.$]ε + x.s1.!t1.$ x.s1.f1.!t1.$ | e | [m.s1.!t1.$]ε + x.s1.!t1.$ x.s1.f2.!t1.$ | e | [m.s1.!t1.$]ε + x.s1.!t1.$ x.s1.[el].!t1.$ | e | [m.s1.!t1.$]ε + x.s1.!t2.$ x.s1.f1.!t2.$ | e | [m.s1.!t2.$]ε + x.s1.!t2.$ x.s1.f2.!t2.$ | e | [m.s1.!t2.$]ε + x.s1.!t2.$ x.s1.[el].!t2.$ | e | [m.s1.!t2.$]ε + x.s1.f1.* x.s1.* | e | [m.s1.f1.*]ε + x.s1.f1.* x.s1.$ | d | [m.s1.f1.*]Δ.$ + x.s1.f1.* x.s1.!t1.$ | d | [m.s1.f1.*]Δ.!t1.$ + x.s1.f1.* x.s1.!t2.$ | d | [m.s1.f1.*]Δ.!t2.$ + x.s1.f1.* x.s1.f1.$ | d | [m.s1.f1.*]Δ.$ + x.s1.f1.* x.s1.f1.!t1.$ | d | [m.s1.f1.*]Δ.!t1.$ + x.s1.f1.* x.s1.f1.!t2.$ | d | [m.s1.f1.*]Δ.!t2.$ + x.s1.f1.$ x.s1.$ | e | [m.s1.f1.$]ε + x.s1.f1.!t1.$ x.s1.!t1.$ | e | [m.s1.f1.!t1.$]ε + x.s1.f1.!t2.$ x.s1.!t2.$ | e | [m.s1.f1.!t2.$]ε + x.s1.f2.* x.s1.* | e | [m.s1.f2.*]ε + x.s1.f2.* x.s1.$ | d | [m.s1.f2.*]Δ.$ + x.s1.f2.* x.s1.!t1.$ | d | [m.s1.f2.*]Δ.!t1.$ + x.s1.f2.* x.s1.!t2.$ | d | [m.s1.f2.*]Δ.!t2.$ + x.s1.f2.* x.s1.f2.$ | d | [m.s1.f2.*]Δ.$ + x.s1.f2.* x.s1.f2.!t1.$ | d | [m.s1.f2.*]Δ.!t1.$ + x.s1.f2.* x.s1.f2.!t2.$ | d | [m.s1.f2.*]Δ.!t2.$ + x.s1.f2.$ x.s1.$ | e | [m.s1.f2.$]ε + x.s1.f2.!t1.$ x.s1.!t1.$ | e | [m.s1.f2.!t1.$]ε + x.s1.f2.!t2.$ x.s1.!t2.$ | e | [m.s1.f2.!t2.$]ε + x.s1.[el].* x.s1.* | e | [m.s1.[el].*]ε + x.s1.[el].* x.s1.$ | d | [m.s1.[el].*]Δ.$ + x.s1.[el].* x.s1.!t1.$ | d | [m.s1.[el].*]Δ.!t1.$ + x.s1.[el].* x.s1.!t2.$ | d | [m.s1.[el].*]Δ.!t2.$ + x.s1.[el].* x.s1.[el].$ | d | [m.s1.[el].*]Δ.$ + x.s1.[el].* x.s1.[el].!t1.$ | d | [m.s1.[el].*]Δ.!t1.$ + x.s1.[el].* x.s1.[el].!t2.$ | d | [m.s1.[el].*]Δ.!t2.$ + x.s1.[el].$ x.s1.$ | e | [m.s1.[el].$]ε + x.s1.[el].!t1.$ x.s1.!t1.$ | e | [m.s1.[el].!t1.$]ε + x.s1.[el].!t2.$ x.s1.!t2.$ | e | [m.s1.[el].!t2.$]ε + x.s2.* x.s2.$ | d | [m.s2.*]Δ.$ + x.s2.* x.s2.!t1.$ | d | [m.s2.*]Δ.!t1.$ + x.s2.* x.s2.!t2.$ | d | [m.s2.*]Δ.!t2.$ + x.s2.* x.s2.f1.* | e | [m.s2.*]ε + x.s2.* x.s2.f1.$ | d | [m.s2.*]Δ.$ + x.s2.* x.s2.f1.!t1.$ | d | [m.s2.*]Δ.!t1.$ + x.s2.* x.s2.f1.!t2.$ | d | [m.s2.*]Δ.!t2.$ + x.s2.* x.s2.f2.* | e | [m.s2.*]ε + x.s2.* x.s2.f2.$ | d | [m.s2.*]Δ.$ + x.s2.* x.s2.f2.!t1.$ | d | [m.s2.*]Δ.!t1.$ + x.s2.* x.s2.f2.!t2.$ | d | [m.s2.*]Δ.!t2.$ + x.s2.* x.s2.[el].* | e | [m.s2.*]ε + x.s2.* x.s2.[el].$ | d | [m.s2.*]Δ.$ + x.s2.* x.s2.[el].!t1.$ | d | [m.s2.*]Δ.!t1.$ + x.s2.* x.s2.[el].!t2.$ | d | [m.s2.*]Δ.!t2.$ + x.s2.* x.s2.*f | e | [m.s2.*]ε + x.s2.$ x.s2.f1.$ | e | [m.s2.$]ε + x.s2.$ x.s2.f2.$ | e | [m.s2.$]ε + x.s2.$ x.s2.[el].$ | e | [m.s2.$]ε + x.s2.!t1.$ x.s2.f1.!t1.$ | e | [m.s2.!t1.$]ε + x.s2.!t1.$ x.s2.f2.!t1.$ | e | [m.s2.!t1.$]ε + x.s2.!t1.$ x.s2.[el].!t1.$ | e | [m.s2.!t1.$]ε + x.s2.!t2.$ x.s2.f1.!t2.$ | e | [m.s2.!t2.$]ε + x.s2.!t2.$ x.s2.f2.!t2.$ | e | [m.s2.!t2.$]ε + x.s2.!t2.$ x.s2.[el].!t2.$ | e | [m.s2.!t2.$]ε + x.s2.f1.* x.s2.* | e | [m.s2.f1.*]ε + x.s2.f1.* x.s2.$ | d | [m.s2.f1.*]Δ.$ + x.s2.f1.* x.s2.!t1.$ | d | [m.s2.f1.*]Δ.!t1.$ + x.s2.f1.* x.s2.!t2.$ | d | [m.s2.f1.*]Δ.!t2.$ + x.s2.f1.* x.s2.f1.$ | d | [m.s2.f1.*]Δ.$ + x.s2.f1.* x.s2.f1.!t1.$ | d | [m.s2.f1.*]Δ.!t1.$ + x.s2.f1.* x.s2.f1.!t2.$ | d | [m.s2.f1.*]Δ.!t2.$ + x.s2.f1.$ x.s2.$ | e | [m.s2.f1.$]ε + x.s2.f1.!t1.$ x.s2.!t1.$ | e | [m.s2.f1.!t1.$]ε + x.s2.f1.!t2.$ x.s2.!t2.$ | e | [m.s2.f1.!t2.$]ε + x.s2.f2.* x.s2.* | e | [m.s2.f2.*]ε + x.s2.f2.* x.s2.$ | d | [m.s2.f2.*]Δ.$ + x.s2.f2.* x.s2.!t1.$ | d | [m.s2.f2.*]Δ.!t1.$ + x.s2.f2.* x.s2.!t2.$ | d | [m.s2.f2.*]Δ.!t2.$ + x.s2.f2.* x.s2.f2.$ | d | [m.s2.f2.*]Δ.$ + x.s2.f2.* x.s2.f2.!t1.$ | d | [m.s2.f2.*]Δ.!t1.$ + x.s2.f2.* x.s2.f2.!t2.$ | d | [m.s2.f2.*]Δ.!t2.$ + x.s2.f2.$ x.s2.$ | e | [m.s2.f2.$]ε + x.s2.f2.!t1.$ x.s2.!t1.$ | e | [m.s2.f2.!t1.$]ε + x.s2.f2.!t2.$ x.s2.!t2.$ | e | [m.s2.f2.!t2.$]ε + x.s2.[el].* x.s2.* | e | [m.s2.[el].*]ε + x.s2.[el].* x.s2.$ | d | [m.s2.[el].*]Δ.$ + x.s2.[el].* x.s2.!t1.$ | d | [m.s2.[el].*]Δ.!t1.$ + x.s2.[el].* x.s2.!t2.$ | d | [m.s2.[el].*]Δ.!t2.$ + x.s2.[el].* x.s2.[el].$ | d | [m.s2.[el].*]Δ.$ + x.s2.[el].* x.s2.[el].!t1.$ | d | [m.s2.[el].*]Δ.!t1.$ + x.s2.[el].* x.s2.[el].!t2.$ | d | [m.s2.[el].*]Δ.!t2.$ + x.s2.[el].$ x.s2.$ | e | [m.s2.[el].$]ε + x.s2.[el].!t1.$ x.s2.!t1.$ | e | [m.s2.[el].!t1.$]ε + x.s2.[el].!t2.$ x.s2.!t2.$ | e | [m.s2.[el].!t2.$]ε + x.*s x.* | e | [m.*s]ε + x.*s x.$ | d | [m.*s]Δ.$ + x.*s x.!t1.$ | d | [m.*s]Δ.!t1.$ + x.*s x.!t2.$ | d | [m.*s]Δ.!t2.$ + x.*s x.f1.* | e | [m.*s]ε + x.*s x.f1.$ | d | [m.*s]Δ.f1.$ + x.*s x.f1.!t1.$ | d | [m.*s]Δ.f1.!t1.$ + x.*s x.f1.!t2.$ | d | [m.*s]Δ.f1.!t2.$ + x.*s x.f2.* | e | [m.*s]ε + x.*s x.f2.$ | d | [m.*s]Δ.f2.$ + x.*s x.f2.!t1.$ | d | [m.*s]Δ.f2.!t1.$ + x.*s x.f2.!t2.$ | d | [m.*s]Δ.f2.!t2.$ + x.*s x.[el].* | e | [m.*s]ε + x.*s x.[el].$ | d | [m.*s]Δ.[el].$ + x.*s x.[el].!t1.$ | d | [m.*s]Δ.[el].!t1.$ + x.*s x.[el].!t2.$ | d | [m.*s]Δ.[el].!t2.$ + x.*s x.s1.* | e | [m.*s]ε + x.*s x.s1.$ | d | [m.*s]Δ.s1.$ + x.*s x.s1.!t1.$ | d | [m.*s]Δ.s1.!t1.$ + x.*s x.s1.!t2.$ | d | [m.*s]Δ.s1.!t2.$ + x.*s x.s1.f1.* | e | [m.*s]ε + x.*s x.s1.f1.$ | d | [m.*s]Δ.s1.f1.$ + x.*s x.s1.f1.!t1.$ | d | [m.*s]Δ.s1.f1.!t1.$ + x.*s x.s1.f1.!t2.$ | d | [m.*s]Δ.s1.f1.!t2.$ + x.*s x.s1.f2.* | e | [m.*s]ε + x.*s x.s1.f2.$ | d | [m.*s]Δ.s1.f2.$ + x.*s x.s1.f2.!t1.$ | d | [m.*s]Δ.s1.f2.!t1.$ + x.*s x.s1.f2.!t2.$ | d | [m.*s]Δ.s1.f2.!t2.$ + x.*s x.s1.[el].* | e | [m.*s]ε + x.*s x.s1.[el].$ | d | [m.*s]Δ.s1.[el].$ + x.*s x.s1.[el].!t1.$ | d | [m.*s]Δ.s1.[el].!t1.$ + x.*s x.s1.[el].!t2.$ | d | [m.*s]Δ.s1.[el].!t2.$ + x.*s x.s2.* | e | [m.*s]ε + x.*s x.s2.$ | d | [m.*s]Δ.s2.$ + x.*s x.s2.!t1.$ | d | [m.*s]Δ.s2.!t1.$ + x.*s x.s2.!t2.$ | d | [m.*s]Δ.s2.!t2.$ + x.*s x.s2.f1.* | e | [m.*s]ε + x.*s x.s2.f1.$ | d | [m.*s]Δ.s2.f1.$ + x.*s x.s2.f1.!t1.$ | d | [m.*s]Δ.s2.f1.!t1.$ + x.*s x.s2.f1.!t2.$ | d | [m.*s]Δ.s2.f1.!t2.$ + x.*s x.s2.f2.* | e | [m.*s]ε + x.*s x.s2.f2.$ | d | [m.*s]Δ.s2.f2.$ + x.*s x.s2.f2.!t1.$ | d | [m.*s]Δ.s2.f2.!t1.$ + x.*s x.s2.f2.!t2.$ | d | [m.*s]Δ.s2.f2.!t2.$ + x.*s x.s2.[el].* | e | [m.*s]ε + x.*s x.s2.[el].$ | d | [m.*s]Δ.s2.[el].$ + x.*s x.s2.[el].!t1.$ | d | [m.*s]Δ.s2.[el].!t1.$ + x.*s x.s2.[el].!t2.$ | d | [m.*s]Δ.s2.[el].!t2.$ + x.*s x.*f | e | [m.*s]ε + x.*s x.s1.*f | e | [m.*s]ε + x.*s x.s2.*f | e | [m.*s]ε + x.*f x.* | d | [m.*f]Δ.* + x.*f x.$ | d | [m.*f]Δ.$ + x.*f x.!t1.$ | d | [m.*f]Δ.!t1.$ + x.*f x.!t2.$ | d | [m.*f]Δ.!t2.$ + x.*f x.f1.* | d | [m.*f]Δ.f1.* + x.*f x.f1.$ | d | [m.*f]Δ.f1.$ + x.*f x.f1.!t1.$ | d | [m.*f]Δ.f1.!t1.$ + x.*f x.f1.!t2.$ | d | [m.*f]Δ.f1.!t2.$ + x.*f x.f2.* | d | [m.*f]Δ.f2.* + x.*f x.f2.$ | d | [m.*f]Δ.f2.$ + x.*f x.f2.!t1.$ | d | [m.*f]Δ.f2.!t1.$ + x.*f x.f2.!t2.$ | d | [m.*f]Δ.f2.!t2.$ + x.*f x.[el].* | d | [m.*f]Δ.[el].* + x.*f x.[el].$ | d | [m.*f]Δ.[el].$ + x.*f x.[el].!t1.$ | d | [m.*f]Δ.[el].!t1.$ + x.*f x.[el].!t2.$ | d | [m.*f]Δ.[el].!t2.$ + x.s1.*f x.s1.* | d | [m.s1.*f]Δ.* + x.s1.*f x.s1.$ | d | [m.s1.*f]Δ.$ + x.s1.*f x.s1.!t1.$ | d | [m.s1.*f]Δ.!t1.$ + x.s1.*f x.s1.!t2.$ | d | [m.s1.*f]Δ.!t2.$ + x.s1.*f x.s1.f1.* | d | [m.s1.*f]Δ.f1.* + x.s1.*f x.s1.f1.$ | d | [m.s1.*f]Δ.f1.$ + x.s1.*f x.s1.f1.!t1.$ | d | [m.s1.*f]Δ.f1.!t1.$ + x.s1.*f x.s1.f1.!t2.$ | d | [m.s1.*f]Δ.f1.!t2.$ + x.s1.*f x.s1.f2.* | d | [m.s1.*f]Δ.f2.* + x.s1.*f x.s1.f2.$ | d | [m.s1.*f]Δ.f2.$ + x.s1.*f x.s1.f2.!t1.$ | d | [m.s1.*f]Δ.f2.!t1.$ + x.s1.*f x.s1.f2.!t2.$ | d | [m.s1.*f]Δ.f2.!t2.$ + x.s1.*f x.s1.[el].* | d | [m.s1.*f]Δ.[el].* + x.s1.*f x.s1.[el].$ | d | [m.s1.*f]Δ.[el].$ + x.s1.*f x.s1.[el].!t1.$ | d | [m.s1.*f]Δ.[el].!t1.$ + x.s1.*f x.s1.[el].!t2.$ | d | [m.s1.*f]Δ.[el].!t2.$ + x.s2.*f x.s2.* | d | [m.s2.*f]Δ.* + x.s2.*f x.s2.$ | d | [m.s2.*f]Δ.$ + x.s2.*f x.s2.!t1.$ | d | [m.s2.*f]Δ.!t1.$ + x.s2.*f x.s2.!t2.$ | d | [m.s2.*f]Δ.!t2.$ + x.s2.*f x.s2.f1.* | d | [m.s2.*f]Δ.f1.* + x.s2.*f x.s2.f1.$ | d | [m.s2.*f]Δ.f1.$ + x.s2.*f x.s2.f1.!t1.$ | d | [m.s2.*f]Δ.f1.!t1.$ + x.s2.*f x.s2.f1.!t2.$ | d | [m.s2.*f]Δ.f1.!t2.$ + x.s2.*f x.s2.f2.* | d | [m.s2.*f]Δ.f2.* + x.s2.*f x.s2.f2.$ | d | [m.s2.*f]Δ.f2.$ + x.s2.*f x.s2.f2.!t1.$ | d | [m.s2.*f]Δ.f2.!t1.$ + x.s2.*f x.s2.f2.!t2.$ | d | [m.s2.*f]Δ.f2.!t2.$ + x.s2.*f x.s2.[el].* | d | [m.s2.*f]Δ.[el].* + x.s2.*f x.s2.[el].$ | d | [m.s2.*f]Δ.[el].$ + x.s2.*f x.s2.[el].!t1.$ | d | [m.s2.*f]Δ.[el].!t1.$ + x.s2.*f x.s2.[el].!t2.$ | d | [m.s2.*f]Δ.[el].!t2.$ diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoAnalysisManager.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoAnalysisManager.kt index a72b68abb..a631929e1 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoAnalysisManager.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoAnalysisManager.kt @@ -29,6 +29,7 @@ import org.opentaint.dataflow.go.analysis.alias.GoLocalAliasAnalysis import org.opentaint.dataflow.go.graph.GoApplicationGraph import org.opentaint.dataflow.go.rules.GoTaintAnalysisContext import org.opentaint.dataflow.go.rules.GoTaintRulesProvider +import org.opentaint.dataflow.go.rules.SelectedGoTaintRulesProvider import org.opentaint.dataflow.go.trace.GoMethodCallPrecondition import org.opentaint.dataflow.go.trace.GoMethodSequentPrecondition import org.opentaint.dataflow.go.trace.GoMethodStartPrecondition @@ -55,6 +56,7 @@ class GoAnalysisManager( ) : GoLanguageManager(cp), TaintAnalysisManager { override val factTypeChecker: FactTypeChecker = FactTypeChecker.Dummy + private val phaseTaintConfig = SelectedGoTaintRulesProvider(taintConfig) private val relevantRuleIds = ConcurrentHashMap.newKeySet() private val contexts = ConcurrentLinkedQueue() @@ -65,8 +67,20 @@ class GoAnalysisManager( override fun selectPhase(phase: Phase) { selectedPhase = phase contexts.forEach { it.resetAnalysisCache() } - if (phase is Phase.FullScan) { - taintConfig.selectRules(relevantRuleIds) + + when (phase) { + is Phase.Prescan -> { + phaseTaintConfig.select(null) + } + + is Phase.ShallowScan -> { + phaseTaintConfig.selectRules(relevantRuleIds) + phaseTaintConfig.select(null) + } + + is Phase.FullScan -> { + phaseTaintConfig.select(phase.actionableRules) + } } } @@ -79,7 +93,7 @@ class GoAnalysisManager( ): MethodAnalysisContext { val taintCtx = GoTaintAnalysisContext( taintAnalysisContext.taintSinkTracker, - taintConfig, + phaseTaintConfig, externalMethodTracker, relevantRuleIds, ) diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/SelectedGoTaintRulesProvider.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/SelectedGoTaintRulesProvider.kt new file mode 100644 index 000000000..7c6af9976 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/SelectedGoTaintRulesProvider.kt @@ -0,0 +1,132 @@ +package org.opentaint.dataflow.go.rules + +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.go.GoFieldSignature +import org.opentaint.dataflow.go.GoFunctionSignature +import org.opentaint.dataflow.go.GoGlobalFieldSignature +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.go.inst.GoIRInst + +class SelectedGoTaintRulesProvider( + private val delegate: GoTaintRulesProvider, +) : GoTaintRulesProvider { + private class SelectedRule { + private val perStatement = hashMapOf>() + + fun find(statement: GoIRInst): List = perStatement[statement] ?: emptyList() + + fun add(statement: GoIRInst, rule: T) { + perStatement.getOrPut(statement) { mutableListOf() }.add(rule) + } + } + + private class SelectedRuleSet { + val globalSource = SelectedRule() + val fieldSource = SelectedRule() + val callSource = SelectedRule() + val callSink = SelectedRule() + } + + @Volatile + private var selected: SelectedRuleSet? = null + + fun select(rules: Map>>?) { + if (rules == null) { + selected = null + return + } + + val selected = SelectedRuleSet() + + for ((inst, instRules) in rules.entries) { + if (inst !is GoIRInst) continue + for ((rule, actions) in instRules) { + if (rule !is TaintRule) continue + + when (rule) { + is TaintRule.GlobalReadSource -> { + val actions = rule.actionsAfter.relevantActions(actions) ?: continue + selected.globalSource.add(inst, rule.copy(actionsAfter = actions)) + } + + is TaintRule.FieldReadSource -> { + val actions = rule.actionsAfter.relevantActions(actions) ?: continue + selected.fieldSource.add(inst, rule.copy(actionsAfter = actions)) + } + + is TaintRule.Source -> { + val actions = rule.actionsAfter.relevantActions(actions) ?: continue + selected.callSource.add(inst, rule.copy(actionsAfter = actions)) + } + + is TaintRule.Sink -> { + check(actions.isEmpty()) { "Sink rule has selected actions: $rule" } + selected.callSink.add(inst, rule) + } + + is TaintRule.Cleaner -> continue + + is TaintRule.PassThrough -> continue + } + } + } + + this.selected = selected + } + + override fun selectRules(ruleIds: Set) { + delegate.selectRules(ruleIds) + } + + override fun sourceRulesForGlobal( + signature: GoGlobalFieldSignature, + statement: GoIRInst, + ): List { + val s = selected ?: return delegate.sourceRulesForGlobal(signature, statement) + return s.globalSource.find(statement) + } + + override fun sourceRulesForFieldRead( + signature: GoFieldSignature, + statement: GoIRInst, + ): List { + val s = selected ?: return delegate.sourceRulesForFieldRead(signature, statement) + return s.fieldSource.find(statement) + } + + override fun sourceRulesForCall( + signature: GoFunctionSignature, + statement: GoIRInst, + allRelevant: Boolean, + ): List { + val s = selected + if (s == null || allRelevant) { + return delegate.sourceRulesForCall(signature, statement, allRelevant) + } + + return s.callSource.find(statement) + } + + override fun sinkRulesForCall( + signature: GoFunctionSignature, + statement: GoIRInst, + ): List { + val s = selected ?: return delegate.sinkRulesForCall(signature, statement) + return s.callSink.find(statement) + } + + override fun passThroughRulesForCall( + signature: GoFunctionSignature, + statement: GoIRInst, + ): List = delegate.passThroughRulesForCall(signature, statement) + + override fun cleanerRulesForCall( + signature: GoFunctionSignature, + statement: GoIRInst, + allRelevant: Boolean, + ): List = delegate.cleanerRulesForCall(signature, statement, allRelevant) + + private fun List.relevantActions(relevant: Set): List? = + filter { it in relevant }.takeIf { it.isNotEmpty() } +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt index ab6174563..fce1d3c89 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt @@ -32,8 +32,10 @@ import org.opentaint.ir.api.jvm.cfg.JIRValue import org.opentaint.ir.api.jvm.cfg.JIRVirtualCallExpr import org.opentaint.ir.api.jvm.ext.findMethodOrNull import org.opentaint.ir.api.jvm.ext.isSubClassOf +import org.opentaint.ir.api.jvm.ext.usedMethods import org.opentaint.ir.impl.cfg.util.isClass import org.opentaint.jvm.util.toJIRClassOrInterface +import org.objectweb.asm.Opcodes import java.util.concurrent.ConcurrentHashMap class JIRCallResolver( @@ -49,20 +51,46 @@ class JIRCallResolver( .forEach { knownLocationIds.add(it.id) } } - private val methodOverridesCache = ConcurrentHashMap>() + private val methodOverridesCache = ConcurrentHashMap, List>() + private val bridgeTargetCache = ConcurrentHashMap>() private fun methodOverrides(method: JIRMethod, baseClass: JIRClassOrInterface): List { if (method.isFinal || method.isConstructor || method.isStatic || method.isClassInitializer) { return emptyList() } - return methodOverridesCache.computeIfAbsent(method) { + return methodOverridesCache.computeIfAbsent(method to baseClass) { val overrides = hierarchy.findOverrides(method, baseClass, knownLocationIds) val knownOverrides = overrides.filter { unitResolver.resolve(it) != UnknownUnit } knownOverrides.ifEmpty { emptyList() } } } + private fun inheritedInterfaceOverrides(method: JIRMethod, baseClass: JIRClassOrInterface): List { + if (!method.isAbstract) return emptyList() + if (!baseClass.isInterface) return emptyList() + + val declaringClass = method.enclosingClass + if (declaringClass == baseClass) return emptyList() + if (!declaringClass.isInterface) return emptyList() + if (declaringClass.declaration.location.id !in knownLocationIds) return emptyList() + + return methodOverrides(method, declaringClass) + } + + private fun bridgeTarget(method: JIRMethod): JIRMethod? { + if (method.access and Opcodes.ACC_BRIDGE == 0) return null + return bridgeTargetCache.computeIfAbsent(method) { + method.usedMethods.mapNotNullTo(mutableListOf()) { target -> + target.takeIf { + target.enclosingClass == method.enclosingClass && + target.name == method.name && + target.description != method.description + } + } + }.singleOrNull() + } + sealed interface MethodResolutionResult { data object MethodResolutionFailed : MethodResolutionResult data class ConcreteMethod(val method: MethodWithContext) : MethodResolutionResult @@ -90,8 +118,9 @@ class JIRCallResolver( fun resolve(call: JIRCallExpr, location: JIRInst, context: JIRMethodAnalysisContext): List { val method = call.method.method val methodIgnored = unitResolver.resolve(method) == UnknownUnit + val declaredMethod = (call as? JIRInstanceCallExpr)?.declaredMethod?.method ?: method - if (methodIgnored && alwaysIgnoreMethod(method)) { + if (alwaysIgnoreMethod(declaredMethod)) { return listOf(MethodResolutionResult.MethodResolutionFailed) } @@ -146,9 +175,11 @@ class JIRCallResolver( result += MethodResolutionResult.Lambda(call, method) } - val overrides = methodOverrides(method, constraint.type).filter { - it.enclosingClass !is JIRLambdaClass // Lambdas handled by JIRLambdaTracker - } + val overrides = methodOverrides(method, constraint.type) + .ifEmpty { inheritedInterfaceOverrides(method, constraint.type) } + .filter { + it.enclosingClass !is JIRLambdaClass // Lambdas handled by JIRLambdaTracker + } overrides.mapTo(methods) { it to constraint } @@ -163,9 +194,11 @@ class JIRCallResolver( } val ctxBuilder = MethodContextCreator(context, call, location, instanceTypeConstraints = null) - val methodsWithContext = methods.flatMapTo(hashSetOf()) { (m, constraint) -> + val methodsWithContext = methods.asSequence().filter { (method, _) -> + ctxBuilder.bridgeArgumentsMayReturnNormally(method) + }.flatMap { (m, constraint) -> ctxBuilder.withInstanceTypeConstraint(constraint).attachContext(m) - } + }.toHashSet() methodsWithContext.mapTo(result) { MethodResolutionResult.ConcreteMethod(it) @@ -308,6 +341,22 @@ class JIRCallResolver( } } + fun bridgeArgumentsMayReturnNormally(method: JIRMethod): Boolean { + val target = bridgeTarget(method) ?: return true + return target.parameters.all { parameter -> + val targetType = parameter.type.toJIRClassOrInterface(cp) ?: return@all true + val constraints = paramTypeConstraints(parameter.index) + constraints.isEmpty() || constraints.any { it.mayBeInstanceOf(targetType) } + } + } + + private fun TypeConstraintInfo.mayBeInstanceOf(target: JIRClassOrInterface): Boolean { + if (type == target || type.isSubClassOf(target)) return true + if (exactType) return false + if (target.isSubClassOf(type)) return true + return type.isInterface || target.isInterface + } + fun attachContext(method: JIRMethod): List { val contextTypeInfo = mutableListOf() if (call is JIRInstanceCallExpr && !method.isConstructor) { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRInstanceTypeMethodContext.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRInstanceTypeMethodContext.kt index 384088807..22d384a3b 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRInstanceTypeMethodContext.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRInstanceTypeMethodContext.kt @@ -1,7 +1,7 @@ package org.opentaint.dataflow.jvm.ap.ifds -import org.opentaint.ir.api.jvm.JIRClassOrInterface import org.opentaint.dataflow.ap.ifds.MethodContext +import org.opentaint.ir.api.jvm.JIRClassOrInterface data class TypeConstraintInfo(val type: JIRClassOrInterface, val exactType: Boolean) diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/TaintConfigUtils.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/TaintConfigUtils.kt index 9c53d5f15..563471385 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/TaintConfigUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/TaintConfigUtils.kt @@ -23,26 +23,34 @@ import org.opentaint.dataflow.taint.TaintFactAwareConditionEvaluator import org.opentaint.dataflow.taint.applyCleanerActions import org.opentaint.util.Maybe import org.opentaint.util.maybeFlatMap +import org.opentaint.util.onSome object TaintConfigUtils { fun applyEntryPointConfig( rules: List>, - taintActionEvaluator: SourceActionEvaluator + taintActionEvaluator: SourceActionEvaluator, + onActionApplied: (TaintEntryPointSource, AssignMark) -> Unit = { _, _ -> }, ) = applyAssignMark( rules, taintActionEvaluator, - TaintEntryPointSource::actionsAfter + TaintEntryPointSource::actionsAfter, + onActionApplied, ) private inline fun applyAssignMark( rules: List>, taintActionEvaluator: SourceActionEvaluator, - actionsAfter: (T) -> List + actionsAfter: (T) -> List, + crossinline onActionApplied: (T, AssignMark) -> Unit, ): Maybe> = rules .applicableRules(conditionEvaluator = null) .maybeFlatMap { item -> actionsAfter(item) .filterIsInstance() - .maybeFlatMap { taintActionEvaluator.accept(item, it) } + .maybeFlatMap { action -> + taintActionEvaluator.accept(item, action).onSome { results -> + if (results.isNotEmpty()) onActionApplied(item, action) + } + } } fun applyPassThrough( diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt index adc8d8297..519f4a335 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt @@ -3,12 +3,21 @@ package org.opentaint.dataflow.jvm.ap.ifds.analysis import mu.KLogger import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.AnalysisRunner +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.ir.api.jvm.ext.allSuperHierarchySequence +import org.opentaint.ir.api.jvm.JIRClassOrInterface +import org.opentaint.dataflow.jvm.ap.ifds.JIRArgumentTypeMethodContext +import org.opentaint.dataflow.jvm.ap.ifds.JIRInstanceTypeMethodContext +import org.opentaint.dataflow.ap.ifds.MethodContext +import org.opentaint.dataflow.ap.ifds.CombinedMethodContext import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.MethodWithContext import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager.Phase import org.opentaint.dataflow.ap.ifds.TaintAnalysisUnitRunner import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction import org.opentaint.dataflow.ap.ifds.analysis.MethodCallResolver @@ -18,6 +27,7 @@ import org.opentaint.dataflow.ap.ifds.analysis.MethodEntrypointResolver import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction import org.opentaint.dataflow.ap.ifds.analysis.MethodSideEffectSummaryHandler import org.opentaint.dataflow.ap.ifds.analysis.MethodStartFlowFunction +import org.opentaint.dataflow.ap.ifds.taint.ActionableRules import org.opentaint.dataflow.ap.ifds.taint.ExternalMethodTracker import org.opentaint.dataflow.ap.ifds.taint.TaintAnalysisContext import org.opentaint.dataflow.ap.ifds.trace.MethodCallPrecondition @@ -30,14 +40,19 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRFactTypeChecker import org.opentaint.dataflow.jvm.ap.ifds.JIRLanguageManager import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalVariableReachability +import org.opentaint.dataflow.jvm.ap.ifds.MethodFlowFunctionUtils import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallFactMapper import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodContextSerializer +import org.opentaint.dataflow.jvm.ap.ifds.LambdaAnonymousClassFeature import org.opentaint.dataflow.jvm.ap.ifds.jIRDowncast import org.opentaint.dataflow.jvm.ap.ifds.taint.JIRTaintAnalysisContext +import org.opentaint.dataflow.jvm.ap.ifds.taint.SelectedTaintRulesProvider import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider import org.opentaint.dataflow.jvm.ap.ifds.trace.JIRMethodCallPrecondition import org.opentaint.dataflow.jvm.ap.ifds.trace.JIRMethodSequentPrecondition import org.opentaint.dataflow.jvm.ap.ifds.trace.JIRMethodStartPrecondition +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.jvm.TaintConfigurationItem import org.opentaint.dataflow.jvm.ifds.JIRUnitResolver import org.opentaint.dataflow.util.RefManager import org.opentaint.ir.api.common.CommonMethod @@ -60,7 +75,69 @@ class JIRAnalysisManager( val externalMethodTracker: ExternalMethodTracker? = null, private val params: Params = Params(), ) : JIRLanguageManager(cp), TaintAnalysisManager { + override val supportsForwardActionableRuleFallback: Boolean = true + + override fun overApproximateMethodContext( + method: MethodWithContext, + contextIndependentFact: Boolean, + ): MethodWithContext { + if (currentPhase !is Phase.ShallowScan) return method + if (!contextIndependentFact) return method + if (method.ctx is EmptyMethodContext || method.ctx.containsLambdaConstraint()) return method + return method.copy(ctx = EmptyMethodContext) + } + + private val contextBoundFunctionTypes = ConcurrentHashMap() + + private fun MethodContext.containsLambdaConstraint(): Boolean = when (this) { + is JIRInstanceTypeMethodContext -> typeConstraint.type.isContextBoundFunction() + is JIRArgumentTypeMethodContext -> typeConstraint.type.isContextBoundFunction() + is CombinedMethodContext -> first.containsLambdaConstraint() || second.containsLambdaConstraint() + else -> false + } + + private fun JIRClassOrInterface.isContextBoundFunction(): Boolean = + contextBoundFunctionTypes.computeIfAbsent(this) { type -> + type is LambdaAnonymousClassFeature.JIRLambdaClass || + (sequenceOf(type) + type.allSuperHierarchySequence).any { superType -> + superType.name.startsWith("kotlin.jvm.functions.Function") || + superType.name.startsWith("kotlin.coroutines.SuspendFunction") || + superType.name.startsWith("java.util.function.") + } + } + + override fun relevantForwardActionableRules( + rules: ActionableRules, + uncoveredSinkRules: Set, + ): ActionableRules { + if (uncoveredSinkRules.isEmpty()) return rules + + val sinkRuleIds = hashSetOf() + for (rule in uncoveredSinkRules) { + val ruleId = (rule as? TaintConfigurationItem)?.serializedId ?: return rules + sinkRuleIds += ruleId + } + + val candidateRuleIds = rules.values + .asSequence() + .flatMap { it.keys.asSequence() } + .mapNotNullTo(hashSetOf()) { (it as? TaintConfigurationItem)?.serializedId } + candidateRuleIds += sinkRuleIds + + val relevantRuleIds = taintConfig.relevantRuleIds(candidateRuleIds) ?: return rules + return buildMap { + rules.forEach { (statement, statementRules) -> + val retainedRules = statementRules.filterTo(linkedMapOf()) { (rule, _) -> + val ruleId = (rule as? TaintConfigurationItem)?.serializedId + ruleId == null || ruleId in relevantRuleIds + } + if (retainedRules.isNotEmpty()) put(statement, retainedRules) + } + } + } + private val refManager = refManager.softRefManager("JIRAnalysisManager") + private val phaseTaintConfig = SelectedTaintRulesProvider(taintConfig) override val factTypeChecker = JIRFactTypeChecker(cp) @@ -77,9 +154,20 @@ class JIRAnalysisManager( override fun selectPhase(phase: Phase) { currentPhase = phase contexts.forEach { it.resetAnalysisCache() } + when (phase) { - Phase.Prescan -> {} - Phase.FullScan -> taintConfig.selectRules(relevantRuleIds) + is Phase.Prescan -> { + phaseTaintConfig.select(null) + } + + is Phase.ShallowScan -> { + phaseTaintConfig.selectRules(relevantRuleIds) + phaseTaintConfig.select(null) + } + + is Phase.FullScan -> { + phaseTaintConfig.select(phase.actionableRules) + } } } @@ -129,7 +217,7 @@ class JIRAnalysisManager( } val taintContext = JIRTaintAnalysisContext( - taintAnalysisContext.taintSinkTracker, taintConfig, externalMethodTracker, relevantRuleIds + taintAnalysisContext.taintSinkTracker, phaseTaintConfig, externalMethodTracker, relevantRuleIds ) return JIRMethodAnalysisContext( @@ -140,6 +228,7 @@ class JIRAnalysisManager( localVariableReachability, aliasAnalysis, taintContext, + callResolver.callResolver, ).also { contexts.add(it) } @@ -327,4 +416,4 @@ class JIRAnalysisManager( val percentValue = current.toDouble() / total return String.format("%.2f", percentValue * 100) + "%" } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt index d9837eac0..deeca7a1a 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt @@ -4,9 +4,16 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import org.opentaint.dataflow.ap.ifds.MethodEntryPoint import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager.Phase import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.ApManager +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFactMapper +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.ap.ifds.trace.MethodCallPrecondition.CallPrecondition +import org.opentaint.dataflow.ap.ifds.trace.MethodSequentPrecondition.SequentPrecondition import org.opentaint.dataflow.jvm.ap.ifds.JIRFactTypeChecker +import org.opentaint.dataflow.jvm.ap.ifds.JIRCallResolver import org.opentaint.dataflow.jvm.ap.ifds.JIRLambdaTracker import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalVariableReachability @@ -14,7 +21,9 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallFactMapper import org.opentaint.dataflow.jvm.ap.ifds.taint.JIRTaintAnalysisContext import org.opentaint.dataflow.util.SoftReferenceManager import org.opentaint.dataflow.util.int2ObjectMap +import org.opentaint.ir.api.common.cfg.CommonInst import java.lang.ref.Reference +import java.util.concurrent.ConcurrentHashMap class JIRMethodAnalysisContext( val analysisManager: JIRAnalysisManager, @@ -24,6 +33,7 @@ class JIRMethodAnalysisContext( val localVariableReachability: JIRLocalVariableReachability, val aliasAnalysis: JIRLocalAliasAnalysis?, val taint: JIRTaintAnalysisContext, + val callResolver: JIRCallResolver, ) : MethodAnalysisContext { init { taint.bindAnalysisContext(this) @@ -31,6 +41,15 @@ class JIRMethodAnalysisContext( val phase: Phase get() = analysisManager.phase + fun recordForwardSourceAction( + statement: CommonInst, + rule: CommonTaintConfigurationItem, + action: CommonTaintAction, + ) { + if (phase !is Phase.ShallowScan) return + taint.taintSinkTracker.recordForwardActionableRule(statement, rule, action) + } + override val methodCallFactMapper: MethodCallFactMapper get() = JIRMethodCallFactMapper @@ -38,6 +57,60 @@ class JIRMethodAnalysisContext( val lambdaCallResolution = Int2ObjectOpenHashMap() + private val rawCallResolutionCache = + int2ObjectMap>() + + private data class TracePreconditionKey( + val apManager: ApManager, + val statementIndex: Int, + val fact: InitialFactAp, + ) + + private class TracePreconditionCache { + val sequent = ConcurrentHashMap>() + val call = ConcurrentHashMap>() + } + + @Volatile + private var tracePreconditionCache: TracePreconditionCache? = null + + private fun tracePreconditionCache(): TracePreconditionCache { + tracePreconditionCache?.let { return it } + return synchronized(this) { + tracePreconditionCache ?: TracePreconditionCache().also { tracePreconditionCache = it } + } + } + + fun cachedSequentTracePrecondition( + apManager: ApManager, + stmtIdx: Int, + fact: InitialFactAp, + compute: () -> Set, + ): Set { + val cache = tracePreconditionCache() + return cache.sequent.computeIfAbsent(TracePreconditionKey(apManager, stmtIdx, fact)) { + compute().toSet() + } + } + + fun cachedCallTracePrecondition( + apManager: ApManager, + stmtIdx: Int, + fact: InitialFactAp, + compute: () -> List, + ): List { + val cache = tracePreconditionCache() + return cache.call.computeIfAbsent(TracePreconditionKey(apManager, stmtIdx, fact)) { + compute().toList() + } + } + + fun cachedRawCallResolution( + stmtIdx: Int, + resolve: () -> List, + ): List = + rawCallResolutionCache.computeIfAbsent(stmtIdx) { resolve() } + fun cachedCallFF(stmtIdx: Int, body: () -> JIRMethodCallFlowFunction): JIRMethodCallFlowFunction = getCallFFCache().computeIfAbsent(stmtIdx) { body() } @@ -64,6 +137,8 @@ class JIRMethodAnalysisContext( taint.reset() lambdaCallResolution.values.forEach { it.resetSubscribers() } taintMarksAssignedOnMethodEnter.clear() + rawCallResolutionCache.clear() + tracePreconditionCache = null callFFCache?.clear() callSHCache?.clear() } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt index 81c6b6ee0..3497b64dc 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt @@ -77,6 +77,13 @@ class JIRMethodCallFlowFunction( this += CallToStartZeroFact } + override fun createFactToFactTransfer( + currentFactAp: FinalFactAp, + ): Set? { + if (factIsRelevantToMethodCall(statement, returnValue, callExpr, currentFactAp)) return null + return setOf(MethodCallFlowFunction.FactToFactTransfer.Unchanged) + } + override fun propagateFact( initialFacts: Set, exclusion: ExclusionSet, @@ -154,6 +161,18 @@ class JIRMethodCallFlowFunction( val callerFact = unmappedCallerFactAp.rebase(startFactBase) val conditionFactReader = FinalFactReader(callerFact, apManager) + val cleanRules = taintCtx.cleanRulesForCallStatement(statement, callExpr, returnValue, callerFact) + if (cleanRules.isEmpty()) { + propagateCleanedFact( + method, + conditionFactReader, + originalFactReader, + addCallToReturn, + startFactBase, + addCallToStart, + ) + return + } val conditionEvaluator = TaintFactAwareConditionEvaluator( listOf(conditionFactReader), @@ -163,7 +182,6 @@ class JIRMethodCallFlowFunction( val cleaner = JIRTaintCleanActionEvaluator(typeResolver) val factReaderBeforeCleaner = FinalFactReader(callerFact, apManager) - val cleanRules = taintCtx.cleanRulesForCallStatement(statement, callExpr, returnValue, callerFact) val cleanerResults = applyCleaner( cleanRules, factReaderBeforeCleaner, @@ -268,6 +286,13 @@ class JIRMethodCallFlowFunction( checker = analysisContext.factTypeChecker ) { callerFact, startFactBase -> val passFactReader = FinalFactReader(callerFact.rebase(startFactBase), apManager) + val passRules = taintCtx.passRulesForCallStatement( + statement, callExpr, returnValue, passFactReader.factAp + ) + if (passRules.isEmpty()) { + trackExternalMethod(startFactBase, method, ruleApplied = false) + return@mapMethodCallToStartFlowFact + } val conditionEvaluator = TaintFactAwareConditionEvaluator( listOf(passFactReader), @@ -278,30 +303,24 @@ class JIRMethodCallFlowFunction( apManager, analysisContext.factTypeChecker, passFactReader, typeResolver ) - val passRules = taintCtx.passRulesForCallStatement(statement, callExpr, returnValue, passFactReader.factAp) val passThroughFacts = applyPassThrough( passRules, conditionEvaluator, passEvaluator ) - if (startFactBase !is AccessPathBase.ClassStatic) { - analysisContext.taint.externalMethodTracker?.let { tracker -> - if (JIRCallResolver.alwaysIgnoreMethod(method)) return@let - - val methodName = "${method.enclosingClass.name}#${method.name}" - val methodDesc = method.description - val factPosition = startFactBase.toString() - val ruleApplied = startFactBase in passEvaluator.relevantPositionBase - tracker.trackExternalMethod(methodName, methodDesc, factPosition, ruleApplied) - } - } + trackExternalMethod( + startFactBase, + method, + ruleApplied = startFactBase in passEvaluator.relevantPositionBase, + ) passThroughFacts.onSome { evaluatedPass -> evaluatedPass.forEach { evp -> val rewrittenFacts = summaryRewriter.rewriteSummaryFact(evp.fact) - for ((unrefinedFact, factRefinement) in rewrittenFacts) { - val fact = factRefinement.refineFact(unrefinedFact) + for (rewritten in rewrittenFacts) { + val factRefinement = rewritten.createFactReader(apManager) + val fact = factRefinement.refineFact(rewritten.fact) passFactReader.updateRefinement(factRefinement) val mappedFact = fact.mapExitToReturnFact() ?: continue @@ -323,13 +342,27 @@ class JIRMethodCallFlowFunction( } } + private fun trackExternalMethod( + startFactBase: AccessPathBase, + method: JIRMethod, + ruleApplied: Boolean, + ) { + if (startFactBase is AccessPathBase.ClassStatic) return + val tracker = analysisContext.taint.externalMethodTracker ?: return + if (JIRCallResolver.alwaysIgnoreMethod(method)) return + + val methodName = "${method.enclosingClass.name}#${method.name}" + tracker.trackExternalMethod(methodName, method.description, startFactBase.toString(), ruleApplied) + } + private fun unresolvedCallDefaultFactPropagation( factAp: FinalFactAp, addCallToReturn: (FinalFactReader, FinalFactAp, TraceInfo?) -> Unit, ) { val rewrittenFacts = summaryRewriter.rewriteSummaryFact(factAp) - for ((unrefinedFact, factRefinement) in rewrittenFacts) { - val fact = factRefinement.refineFact(unrefinedFact) + for (rewritten in rewrittenFacts) { + val factRefinement = rewritten.createFactReader(apManager) + val fact = factRefinement.refineFact(rewritten.fact) addCallToReturn(factRefinement, fact, null) } } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallResolver.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallResolver.kt index 78fe7fa16..1bba26a19 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallResolver.kt @@ -69,8 +69,7 @@ class JIRMethodCallResolver( handler: MethodCallHandler, failureHandler: MethodAnalyzer.MethodCallResolutionFailureHandler ) { - val callees = callResolver.resolve(callExpr, location, callerContext) - + val callees = resolveCall(callerContext, callExpr, location) val analyzer = runner.getMethodAnalyzer(callerContext.methodEntryPoint) for (resolvedCallee in callees) { resolveJirMethodCall(callerContext, resolvedCallee, analyzer, callExpr, location, failureHandler, handler) @@ -161,12 +160,21 @@ class JIRMethodCallResolver( callExpr: JIRCallExpr, location: JIRInst ): List { - val callees = callResolver.resolve(callExpr, location, callerContext) + val callees = resolveCall(callerContext, callExpr, location) return callees.flatMap { resolvedCallee -> resolvedJirMethodCalls(callerContext, location, resolvedCallee) } } + private fun resolveCall( + callerContext: JIRMethodAnalysisContext, + callExpr: JIRCallExpr, + location: JIRInst, + ): List = + callerContext.cachedRawCallResolution(location.location.index) { + callResolver.resolve(callExpr, location, callerContext) + } + private fun resolvedJirMethodCalls( callerContext: JIRMethodAnalysisContext, location: JIRInst, diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt index 3aec7b58e..930d5fa3f 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt @@ -26,6 +26,18 @@ class JIRMethodCallRuleBasedSummaryRewriter( private val analysisContext: JIRMethodAnalysisContext, private val apManager: ApManager ) { + internal class RewrittenFact( + val fact: FinalFactAp, + private val refinement: FinalFactReader?, + ) { + val isIdentity: Boolean get() = refinement == null + + fun createFactReader(apManager: ApManager): FinalFactReader = + refinement?.copy() ?: FinalFactReader(fact, apManager) + } + + private val rewrittenFacts = hashMapOf>() + private val taintCtx get() = analysisContext.taint private val callExpr by lazy { @@ -86,10 +98,14 @@ class JIRMethodCallRuleBasedSummaryRewriter( result } - fun rewriteSummaryFact(fact: FinalFactAp): List> { - val startFactReader = FinalFactReader(fact, apManager) + internal fun rewriteSummaryFact(fact: FinalFactAp): List = + rewrittenFacts.getOrPut(fact) { rewriteSummaryFactUncached(fact) } + + private fun rewriteSummaryFactUncached(fact: FinalFactAp): List { val actionsForBase = userRuleDefinedActions[fact.base].orEmpty() - if (actionsForBase.isEmpty()) return listOf(fact to startFactReader) + if (actionsForBase.isEmpty()) return listOf(RewrittenFact(fact, refinement = null)) + + val startFactReader = FinalFactReader(fact, apManager) val cleanEvaluator = JIRTaintCleanActionEvaluator(typeResolver) val cleanedFact = actionsForBase.entries.applyCleanerActions( @@ -108,7 +124,11 @@ class JIRMethodCallRuleBasedSummaryRewriter( return cleanedFact.mapNotNull { val resultFact = it.fact ?: return@mapNotNull null - resultFact.factAp to resultFact + if (!resultFact.hasRefinement && resultFact.factAp == fact) { + RewrittenFact(fact, refinement = null) + } else { + RewrittenFact(resultFact.factAp, resultFact) + } } } } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallSummaryHandler.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallSummaryHandler.kt index 865093a7b..d6ef6cf20 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallSummaryHandler.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallSummaryHandler.kt @@ -69,24 +69,40 @@ class JIRMethodCallSummaryHandler( } override fun prepareFactToFactSummary(summaryEdge: Edge.FactToFact): List = - summaryRewriter.rewriteSummaryFact(summaryEdge.factAp).map { (resultFact, refinement) -> - Edge.FactToFact( - summaryEdge.methodEntryPoint, - refinement.refineFact(summaryEdge.initialFactAp), - summaryEdge.statement, - refinement.refineFact(resultFact) - ) + summaryRewriter.rewriteSummaryFact(summaryEdge.factAp).map { rewritten -> + if (rewritten.isIdentity) return@map summaryEdge + + val refinement = rewritten.createFactReader(apManager) + val initialFact = refinement.refineFact(summaryEdge.initialFactAp) + val finalFact = refinement.refineFact(rewritten.fact) + if (initialFact == summaryEdge.initialFactAp && finalFact == summaryEdge.factAp) { + summaryEdge + } else { + Edge.FactToFact( + summaryEdge.methodEntryPoint, + initialFact, + summaryEdge.statement, + finalFact, + ) + } } override fun prepareNDFactToFactSummary(summaryEdge: Edge.NDFactToFact): List = - summaryRewriter.rewriteSummaryFact(summaryEdge.factAp).map { (resultFact, refinement) -> + summaryRewriter.rewriteSummaryFact(summaryEdge.factAp).map { rewritten -> + if (rewritten.isIdentity) return@map summaryEdge + + val refinement = rewritten.createFactReader(apManager) check(!refinement.hasRefinement) { "Can't refine NDF2F edge" } - Edge.NDFactToFact( - summaryEdge.methodEntryPoint, - summaryEdge.initialFacts, - summaryEdge.statement, - resultFact, - ) + if (rewritten.fact == summaryEdge.factAp) { + summaryEdge + } else { + Edge.NDFactToFact( + summaryEdge.methodEntryPoint, + summaryEdge.initialFacts, + summaryEdge.statement, + rewritten.fact, + ) + } } private fun applyCallAliases(fact: FinalFactAp, body: (FinalFactAp) -> Unit) { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodSequentFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodSequentFlowFunction.kt index ba504e00e..aa231fae4 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodSequentFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodSequentFlowFunction.kt @@ -12,6 +12,7 @@ import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction +import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction.FactToFactTransfer import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction.Sequent import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction.TraceInfo import org.opentaint.dataflow.jvm.ap.ifds.MethodFlowFunctionUtils @@ -112,6 +113,30 @@ class JIRMethodSequentFlowFunction( ) } + override fun createFactToFactTransfer(currentFactAp: FinalFactAp): Set? { + if (currentInst is JIRReturnInst || currentInst is JIRThrowInst) return null + + return buildSet { + propagate( + initialFacts = null, + factAp = currentFactAp, + unchanged = { add(FactToFactTransfer.Unchanged) }, + propagateFact = { fact, trace -> + add(FactToFactTransfer.Fact(fact, trace)) + }, + propagateFactWithRefinement = { _, _, _ -> + error("Fact refinement is only valid at a method exit") + }, + propagateFactWithAccessorExclude = { fact, accessor, trace -> + add(FactToFactTransfer.ExcludeAccessor(fact.excludeField(accessor), accessor, trace)) + }, + sideEffect = { + error("A non-exit sequential transfer cannot produce a side effect") + }, + ) + } + } + override fun propagateNDFactToFact( initialFacts: Set, currentFactAp: FinalFactAp @@ -690,6 +715,9 @@ class JIRMethodSequentFlowFunction( val sourceRule = sourceRuleWithCondition.rule for (action in sourceRule.actionsAfter) { sourceEvaluator.accept(sourceRule, action).onSome { evaluatedFacts -> + if (!generateTrace && evaluatedFacts.isNotEmpty()) { + analysisContext.recordForwardSourceAction(currentInst, sourceRule, action) + } val trace = TraceInfo.Rule(sourceRule, action) evaluatedFacts.mapTo(this) { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodStartFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodStartFlowFunction.kt index 82ab4ecb1..2d6c11217 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodStartFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodStartFlowFunction.kt @@ -38,7 +38,9 @@ class JIRMethodStartFlowFunction( ) val rules = context.taint.sourceRulesForMethodEntry(context.methodEntryPoint.statement as JIRInst, fact = null) - applyEntryPointConfig(rules, sourceEvaluator).onSome { facts -> + applyEntryPointConfig(rules, sourceEvaluator) { rule, action -> + context.recordForwardSourceAction(context.methodEntryPoint.statement, rule, action) + }.onSome { facts -> facts.mapTo(result) { it.getAllAccessors() .filterIsInstanceTo(context.taintMarksAssignedOnMethodEnter) diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt index 393db42f4..7442a4682 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt @@ -158,6 +158,9 @@ class JIRMethodCallTaintUtil( sourceEvaluator: TaintSourceActionEvaluator, createFinalFact: (FinalFactAp, TraceInfo) -> Unit ) = applySourceAction(rule, rule.actionsAfter, sourceEvaluator) { f, action -> + if (!generateTrace) { + analysisContext.recordForwardSourceAction(statement, rule, action) + } val trace = TraceInfo.Rule(rule, action) createFinalFact(f, trace) } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRSequentTaintUtil.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRSequentTaintUtil.kt index 5e6a820e7..06199e5b5 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRSequentTaintUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRSequentTaintUtil.kt @@ -129,6 +129,9 @@ class JIRSequentTaintUtil( ) { for (action in rule.actionsAfter) { sourceEvaluator.accept(rule, action).onSome { facts -> + if (!generateTrace && facts.isNotEmpty()) { + analysisContext.recordForwardSourceAction(statement, rule, action) + } val trace = Rule(rule, action) facts.forEach { createFinalFact(it, trace) } } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt index d5311836c..f4f03dde6 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt @@ -108,17 +108,30 @@ class JIRTaintAnalysisContext( rules: Iterable, cond: T.() -> Condition, statement: JIRInst, callExpr: JIRCallExpr, returnValue: JIRImmediate?, ): List> { - val conditionRewriter = JIRMarkAwareConditionRewriter( - CallPositionToJIRValueResolver(callExpr, returnValue), - analysisContext, statement - ) - - return rules.mapNotNull { - val cond = conditionRewriter.rewrite(it.cond()) - if (cond.isFalse) return@mapNotNull null - - RuleWithCondition(it, cond) - }.handlePhase() + val iterator = rules.iterator() + if (!iterator.hasNext()) return emptyList() + var conditionRewriter: JIRMarkAwareConditionRewriter? = null + + val result = arrayListOf>() + do { + val rule = iterator.next() + val condition = rule.cond() + val rewrittenCondition = if (condition.isTrue()) { + RuleConditionRewriter.trueExpr + } else { + val rewriter = conditionRewriter ?: JIRMarkAwareConditionRewriter( + CallPositionToJIRValueResolver(callExpr, returnValue), + analysisContext, + statement, + ).also { conditionRewriter = it } + rewriter.rewrite(condition) + } + if (!rewrittenCondition.isFalse) { + result += RuleWithCondition(rule, rewrittenCondition) + } + } while (iterator.hasNext()) + + return result.handlePhase() } fun sourceRulesForStaticField( @@ -171,18 +184,30 @@ class JIRTaintAnalysisContext( rules: Iterable, cond: T.() -> Condition, statement: JIRInst, ): List> { - val method = statement.location.method - val valueResolver = CalleePositionToJIRValueResolver(method) - val conditionRewriter = JIRMarkAwareConditionRewriter( - valueResolver, analysisContext, statement - ) - - return rules.mapNotNull { - val cond = conditionRewriter.rewrite(it.cond()) - if (cond.isFalse) return@mapNotNull null - - RuleWithCondition(it, cond) - }.handlePhase() + val iterator = rules.iterator() + if (!iterator.hasNext()) return emptyList() + var conditionRewriter: JIRMarkAwareConditionRewriter? = null + + val result = arrayListOf>() + do { + val rule = iterator.next() + val condition = rule.cond() + val rewrittenCondition = if (condition.isTrue()) { + RuleConditionRewriter.trueExpr + } else { + val rewriter = conditionRewriter ?: JIRMarkAwareConditionRewriter( + CalleePositionToJIRValueResolver(statement.location.method), + analysisContext, + statement, + ).also { conditionRewriter = it } + rewriter.rewrite(condition) + } + if (!rewrittenCondition.isFalse) { + result += RuleWithCondition(rule, rewrittenCondition) + } + } while (iterator.hasNext()) + + return result.handlePhase() } private fun List>.handlePhase() = diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/SelectedTaintRulesProvider.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/SelectedTaintRulesProvider.kt new file mode 100644 index 000000000..e94af7503 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/SelectedTaintRulesProvider.kt @@ -0,0 +1,234 @@ +package org.opentaint.dataflow.jvm.ap.ifds.taint + +import org.opentaint.dataflow.ap.ifds.access.FactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.jvm.Action +import org.opentaint.dataflow.configuration.jvm.TaintCleaner +import org.opentaint.dataflow.configuration.jvm.TaintConfigurationItem +import org.opentaint.dataflow.configuration.jvm.TaintEntryPointSource +import org.opentaint.dataflow.configuration.jvm.TaintMethodEntrySink +import org.opentaint.dataflow.configuration.jvm.TaintMethodExitSink +import org.opentaint.dataflow.configuration.jvm.TaintMethodExitSource +import org.opentaint.dataflow.configuration.jvm.TaintMethodSink +import org.opentaint.dataflow.configuration.jvm.TaintMethodSource +import org.opentaint.dataflow.configuration.jvm.TaintPassThrough +import org.opentaint.dataflow.configuration.jvm.TaintStaticFieldSource +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.jvm.JIRField +import org.opentaint.ir.api.jvm.cfg.JIRInst + +class SelectedTaintRulesProvider( + private val delegate: TaintRulesProvider, +) : TaintRulesProvider { + private class SelectedRule { + private val perStatement = hashMapOf>() + fun find(statement: JIRInst): List = perStatement[statement] ?: emptyList() + fun add(statement: JIRInst, rule: T) { + perStatement.getOrPut(statement) { mutableListOf() }.add(rule) + } + } + + private class SelectedRuleSet { + val methodSource = SelectedRule() + val methodEntrySource = SelectedRule() + val methodExitSource = SelectedRule() + val staticFieldSource = SelectedRule() + + val methodSink = SelectedRule() + val methodEntrySink = SelectedRule() + val methodExitSink = SelectedRule() + } + + @Volatile + private var selected: SelectedRuleSet? = null + + fun select(rules: Map>>?) { + if (rules == null) { + selected = null + return + } + + val selected = SelectedRuleSet() + + for ((inst, instRules) in rules.entries) { + if (inst !is JIRInst) continue + for ((rule, actions) in instRules) { + if (rule !is TaintConfigurationItem) continue + + when (rule) { + is TaintMethodSource -> { + val actions = rule.actionsAfter.relevantActions(actions) ?: continue + val selectedRule = rule.copy(actionsAfter = actions) + selected.methodSource.add(inst, selectedRule) + } + + is TaintCleaner -> continue + + is TaintMethodEntrySink -> { + check(actions.isEmpty()) { "Sink rule has selected actions: $rule" } + selected.methodEntrySink.add(inst, rule) + } + + is TaintMethodExitSink -> { + check(actions.isEmpty()) { "Sink rule has selected actions: $rule" } + selected.methodExitSink.add(inst, rule) + } + + is TaintMethodSink -> { + check(actions.isEmpty()) { "Sink rule has selected actions: $rule" } + selected.methodSink.add(inst, rule) + } + + is TaintEntryPointSource -> { + val actions = rule.actionsAfter.relevantActions(actions) ?: continue + val selectedRule = rule.copy(actionsAfter = actions) + selected.methodEntrySource.add(inst, selectedRule) + } + + is TaintMethodExitSource -> { + val actions = rule.actionsAfter.relevantActions(actions) ?: continue + val selectedRule = rule.copy(actionsAfter = actions) + selected.methodExitSource.add(inst, selectedRule) + } + + is TaintStaticFieldSource -> { + val actions = rule.actionsAfter.relevantActions(actions) ?: continue + val selectedRule = rule.copy(actionsAfter = actions) + selected.staticFieldSource.add(inst, selectedRule) + } + + is TaintPassThrough -> continue + } + } + } + + this.selected = selected + } + + override fun selectRules(ruleIds: Set) { + delegate.selectRules(ruleIds) + } + + override fun relevantRuleIds(candidateRuleIds: Set): Set? = + delegate.relevantRuleIds(candidateRuleIds) + + override fun entryPointRulesForMethod( + method: CommonMethod, + statement: CommonInst, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable { + val s = selected + if (s == null || allRelevant) { + return delegate.entryPointRulesForMethod(method, statement, fact, allRelevant) + } + + return s.methodEntrySource.find(statement as JIRInst) + } + + override fun sinkRulesForMethodEntry( + method: CommonMethod, + statement: CommonInst, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable { + val s = selected + if (s == null || allRelevant) { + return delegate.sinkRulesForMethodEntry(method, statement, fact, allRelevant) + } + + return s.methodEntrySink.find(statement as JIRInst) + } + + override fun sourceRulesForMethod( + method: CommonMethod, + statement: CommonInst, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable { + val s = selected + if (s == null || allRelevant) { + return delegate.sourceRulesForMethod(method, statement, fact, allRelevant) + } + + return s.methodSource.find(statement as JIRInst) + } + + override fun exitSourceRulesForMethod( + method: CommonMethod, + statement: CommonInst, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable { + val s = selected + if (s == null || allRelevant) { + return delegate.exitSourceRulesForMethod(method, statement, fact, allRelevant) + } + + return s.methodExitSource.find(statement as JIRInst) + } + + override fun sourceRulesForStaticField( + field: JIRField, + statement: CommonInst, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable { + val s = selected + if (s == null || allRelevant) { + return delegate.sourceRulesForStaticField(field, statement, fact, allRelevant) + } + + return s.staticFieldSource.find(statement as JIRInst) + } + + override fun sinkRulesForMethod( + method: CommonMethod, + statement: CommonInst, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable { + val s = selected + if (s == null || allRelevant) { + return delegate.sinkRulesForMethod(method, statement, fact, allRelevant) + } + + return s.methodSink.find(statement as JIRInst) + } + + override fun sinkRulesForMethodExit( + method: CommonMethod, + statement: CommonInst, + fact: FactAp?, + initialFacts: Set?, + allRelevant: Boolean, + ): Iterable { + val s = selected + if (s == null || allRelevant) { + return delegate.sinkRulesForMethodExit(method, statement, fact, initialFacts, allRelevant) + } + + return s.methodExitSink.find(statement as JIRInst) + } + + override fun cleanerRulesForMethod( + method: CommonMethod, + statement: CommonInst, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable = delegate.cleanerRulesForMethod(method, statement, fact, allRelevant) + + override fun passTroughRulesForMethod( + method: CommonMethod, + statement: CommonInst?, + fact: FactAp?, + allRelevant: Boolean, + ): Iterable = + delegate.passTroughRulesForMethod(method, statement, fact, allRelevant) + + private fun List.relevantActions(relevant: Set): List? = + filter { it in relevant }.takeIf { it.isNotEmpty() } +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintRulesProvider.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintRulesProvider.kt index d54783773..4da1f985c 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintRulesProvider.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintRulesProvider.kt @@ -28,4 +28,7 @@ interface TaintRulesProvider : CommonTaintRulesProvider { fun sourceRulesForStaticField(field: JIRField, statement: CommonInst, fact: FactAp?, allRelevant: Boolean = false): Iterable fun selectRules(ruleIds: Set) + + /** Retains complete rule-graph paths from the supplied candidate rule IDs, or returns null without a rule graph. */ + fun relevantRuleIds(candidateRuleIds: Set): Set? = null } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt index 365e722c1..afd966602 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt @@ -43,17 +43,25 @@ class JIRMethodCallPrecondition( private val taintCtx get() = analysisContext.taint - override fun factPrecondition(fact: InitialFactAp): List { - val results = mutableListOf() + override fun factPrecondition(fact: InitialFactAp): List = + analysisContext.cachedCallTracePrecondition(apManager, statement.location.index, fact) { + val results = mutableListOf() + addFactPreconditions(results, fact) - results += preconditionForFact(fact)?.let { PreconditionFactsForInitialFact(fact, it) } - ?: CallPrecondition.Unchanged + analysisContext.aliasAnalysis?.forEachPossibleAliasAtStatement(statement, fact) { aliasedFact -> + addFactPreconditions(results, aliasedFact) + } - analysisContext.aliasAnalysis?.forEachPossibleAliasAtStatement(statement, fact) { aliasedFact -> - preconditionForFact(aliasedFact)?.let { results += PreconditionFactsForInitialFact(aliasedFact, it) } + results } - return results + private fun addFactPreconditions( + results: MutableList, + fact: InitialFactAp, + ) { + val callPreconditions = preconditionForFact(fact) + results += callPreconditions?.let { PreconditionFactsForInitialFact(fact, it) } + ?: CallPrecondition.Unchanged } override fun factPreconditionResolutionFailure( diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodSequentPrecondition.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodSequentPrecondition.kt index 51d93d44b..484bd1080 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodSequentPrecondition.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodSequentPrecondition.kt @@ -42,14 +42,18 @@ class JIRMethodSequentPrecondition( override fun factPrecondition( fact: InitialFactAp, - ): Set { + ): Set = analysisContext.cachedSequentTracePrecondition( + apManager, + currentInst.location.index, + fact, + ) { if (currentInst !is JIRAssignInst && currentInst !is JIRReturnInst && currentInst !is JIRThrowInst) { - return setOf(SequentPrecondition.Unchanged) + return@cachedSequentTracePrecondition setOf(SequentPrecondition.Unchanged) } val results = mutableSetOf() results.computeFactPrecondition(fact, applyExitSourceRules = true) - return results + results } private fun MutableSet.computeFactPrecondition( diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/HybridActionableRuleSelection.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/HybridActionableRuleSelection.kt new file mode 100644 index 000000000..f2ac82c7f --- /dev/null +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/HybridActionableRuleSelection.kt @@ -0,0 +1,19 @@ +package org.opentaint.common.sast.dataflow + +import org.opentaint.dataflow.ap.ifds.trace.action.ActionableRulesCollectionResult + +internal fun actionableRulesWithFallback( + searchResults: List, + fallback: (unprocessedIndices: List) -> ActionableRulesCollectionResult.Collected?, +): List { + val collected = searchResults + .filterIsInstance() + .toMutableList() + val unprocessedIndices = searchResults.indices.filter { index -> + searchResults[index] === ActionableRulesCollectionResult.Unprocessed + } + if (unprocessedIndices.isEmpty()) return collected + + fallback(unprocessedIndices)?.let(collected::add) + return collected +} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt index 226b5ff9a..d2e63d833 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt @@ -11,7 +11,9 @@ import org.opentaint.dataflow.ap.ifds.FieldAccessor import org.opentaint.dataflow.ap.ifds.FinalAccessor import org.opentaint.dataflow.ap.ifds.MethodEntryPoint import org.opentaint.dataflow.ap.ifds.MethodStats +import org.opentaint.dataflow.ap.ifds.MethodTaintMarkState import org.opentaint.dataflow.ap.ifds.MethodWithContext +import org.opentaint.dataflow.ap.ifds.TaintMarkTransition import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager import org.opentaint.dataflow.ap.ifds.TaintAnalysisUnitRunnerManager import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor @@ -20,20 +22,28 @@ import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor import org.opentaint.dataflow.ap.ifds.ValueAccessor import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy.AnyAccessorDisabled +import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.ApMode import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.automata.AutomataApManager +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager import org.opentaint.dataflow.ap.ifds.access.cactus.CactusApManager import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext +import org.opentaint.dataflow.ap.ifds.taint.ActionableRules import org.opentaint.dataflow.ap.ifds.taint.ExternalMethodTracker import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker +import org.opentaint.dataflow.ap.ifds.trace.ExactProcessingTimeBudget import org.opentaint.dataflow.ap.ifds.trace.InnerCallTraceResolveStrategy import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntryAction.TraceSummaryEdge import org.opentaint.dataflow.ap.ifds.trace.TraceResolver import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace +import org.opentaint.dataflow.ap.ifds.trace.action.ActionableRulesCollectionResult +import org.opentaint.dataflow.ap.ifds.trace.action.mergeActionableRules import org.opentaint.dataflow.ap.ifds.trace.path.TracePathGenerationResult import org.opentaint.dataflow.ap.ifds.trace.path.TracePathResolveParams +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem import org.opentaint.dataflow.configuration.jvm.TaintSinkMeta import org.opentaint.dataflow.ifds.UnitResolver import org.opentaint.dataflow.util.Cancellation @@ -90,6 +100,8 @@ abstract class TaintAnalyzer( ApMode.Tree -> TreeApManager(unrollStrategy, refManager, cancellation) ApMode.Cactus -> CactusApManager(unrollStrategy, cancellation) ApMode.Automata -> AutomataApManager(unrollStrategy, cancellation) + ApMode.BaseOnly -> BaseOnlyApManager(unrollStrategy, cancellation, fieldSensitive = false) + ApMode.BaseOnlyField -> BaseOnlyApManager(unrollStrategy, cancellation, fieldSensitive = true) } } @@ -124,8 +136,14 @@ abstract class TaintAnalyzer( prescan(startMethods) logger.info { "Finish prescan phase" } + logger.info { "Start shallow scan phase" } + val (actionableRules, status) = shallowScan(analysisStart, entryPoints, startMethods) + logger.info { "Finish shallow scan phase" } + + if (actionableRules.isEmpty()) return emptyList() to status + logger.info { "Start full scan phase" } - val fullScanResult = fullScan(analysisStart, entryPoints, startMethods) + val fullScanResult = fullScan(analysisStart, entryPoints, startMethods, actionableRules) logger.info { "Finish full scan phase" } return fullScanResult } @@ -145,13 +163,196 @@ abstract class TaintAnalyzer( } } + private fun shallowScan( + analysisStart: TimeSource.Monotonic.ValueTimeMark, + entryPoints: List, + startMethods: List, + ): Pair, Status> { + val shallowScanManager = when (options.shallowScanApMode) { + ApMode.Tree -> TreeApManager(unrollStrategy, refManager, cancellation) + ApMode.Cactus -> CactusApManager(unrollStrategy, cancellation) + ApMode.Automata -> AutomataApManager(unrollStrategy, cancellation) + ApMode.BaseOnly -> BaseOnlyApManager(unrollStrategy, cancellation, fieldSensitive = false) + ApMode.BaseOnlyField -> BaseOnlyApManager(unrollStrategy, cancellation, fieldSensitive = true) + } + analysisManager.selectPhase(TaintAnalysisManager.Phase.ShallowScan) + ifdsEngine.resetApManager(shallowScanManager) + + val analysisTimeout = (options.ifdsTimeout - analysisStart.elapsedNow()) * 0.40 + runCatching { ifdsEngine.runAnalysis(startMethods, timeout = analysisTimeout, cancellationTimeout = 30.seconds) } + .onFailure { logger.error(it) { "Shallow analysis failed" } } + + val analysisStatus = ifdsEngine.status.get() + + ifdsEngine.cleanup() + + val allVulnerabilities = ifdsEngine.getVulnerabilities() + + logger.info { "Start shallow scan discovery confirmation" } + val vulnCheckTimeout = options.ifdsTimeout - analysisStart.elapsedNow() + var vulnerabilities = ifdsEngine.confirmVulnerabilities( + entryPoints.toHashSet(), allVulnerabilities, + vulnCheckTimeout, cancellationTimeout = 30.seconds + ) + + logger.info { "Total shallow scan discoveries: ${vulnerabilities.size}" } + + if (options.debugOptions?.enableVulnSummary == true) { + logger.info { + printVulnSummary(vulnerabilities) + } + } + + if (options.analysisCwe != null) { + vulnerabilities = vulnerabilities.filter { + val cwe = (it.rule.meta as TaintSinkMeta).cwe + cwe?.intersect(options.analysisCwe)?.isNotEmpty() ?: true + } + + logger.info { "Shallow scan discoveries with cwe ${options.analysisCwe}: ${vulnerabilities.size}" } + } + + logger.info { "Start actionable rules discovery" } + val ruleDiscoveryTimeout = (options.ifdsTimeout - analysisStart.elapsedNow()) * 0.5 + + val ruleSearchResults = ifdsEngine.resolveActionableRules( + shallowScanManager, + entryPoints, + vulnerabilities, + ruleDiscoveryTimeout, + ).also { logger.info { "Finish actionable rules discovery" } } + + check(ruleSearchResults.size == vulnerabilities.size) { + "Actionable rule search result count does not match vulnerability count" + } + + val invalidTraces = ruleSearchResults.count { it === ActionableRulesCollectionResult.Failed } + if (invalidTraces > 0) { + logger.info { "Filter out $invalidTraces discoveries with invalid traces" } + } + + val successfullyResolvedRules = actionableRulesWithFallback(ruleSearchResults) { unprocessedIndices -> + val uncoveredVulnerabilities = unprocessedIndices.map(vulnerabilities::get) + if (analysisManager.supportsForwardActionableRuleFallback) { + logger.info { + "Use forward actionable rule fallback for ${uncoveredVulnerabilities.size} unprocessed discoveries" + } + ActionableRulesCollectionResult.Collected(forwardActionableRules(uncoveredVulnerabilities)) + } else { + logger.info { "Filter out ${uncoveredVulnerabilities.size} discoveries without traces" } + null + } + } + + val ruleDiscoveryStatus = ifdsEngine.status.get() + val status = Status(analysisStatus, ruleDiscoveryStatus) + + return successfullyResolvedRules to status + } + + private fun forwardActionableRules( + vulnerabilities: List, + ): ActionableRules { + val forwardRules = ifdsEngine.getForwardActionableRules() + val result = linkedMapOf< + CommonInst, + MutableMap>, + >() + val summaryStats = ifdsEngine.methodTaintMarkSummaryStats() + + vulnerabilities.forEach { vulnerability -> + val sinkMethod = vulnerability.statement.location.method + val reachableMethods = ifdsEngine.methodsThatCanReach(sinkMethod) + val relevantRules = analysisManager.relevantForwardActionableRules( + forwardRules, + vulnerability.vulnerabilityRules.keys, + ) + val ruleTransitions = hashMapOf>() + val relevantMarks = hashSetOf() + for ((statement, statementRules) in relevantRules) { + for ((rule, actions) in statementRules) { + val flow = rule.taintRuleMarkFlow(actions) + relevantMarks += flow.inputMarks + relevantMarks += flow.outputMarks + if (!flow.outputMarksComplete) continue + val methodTransitions = ruleTransitions.getOrPut(statement.location.method, ::hashSetOf) + flow.inputMarks.forEach { inputMark -> + flow.outputMarks.forEach { outputMark -> + methodTransitions += TaintMarkTransition(inputMark, outputMark) + } + } + } + } + val sinkMarks = vulnerability.vulnerabilityRules.keys.flatMapTo(hashSetOf()) { sinkRule -> + sinkRule.taintRuleMarkFlow(emptySet()).inputMarks + } + relevantMarks += sinkMarks + val markReachableStates = if (sinkMarks.isEmpty()) { + null + } else { + ifdsEngine.taintMarkStatesThatCanReach( + sinkMethod, + sinkMarks, + ruleTransitions, + relevantMarks, + ) + } + var candidates = 0 + var retained = 0 + + for ((statement, statementRules) in relevantRules) { + candidates += statementRules.size + if (statement.location.method !in reachableMethods) continue + + for ((rule, actions) in statementRules) { + val flow = rule.taintRuleMarkFlow(actions) + val markReachable = markReachableStates == null || + !flow.outputMarksComplete || + flow.outputMarks.isEmpty() || + flow.outputMarks.any { outputMark -> + MethodTaintMarkState(statement.location.method, outputMark) in markReachableStates + } + if (!markReachable) continue + + val targetRules = result.getOrPut(statement) { linkedMapOf() } + targetRules.getOrPut(rule, ::linkedSetOf).addAll(actions) + retained++ + } + } + + logger.debug { + "Forward actionable rule mark-reachability filter for $sinkMethod: " + + "$retained/$candidates source rules, ${markReachableStates?.size ?: 0} mark states, " + + "${relevantMarks.size} relevant marks, ${reachableMethods.size} methods; " + + "summaries: ${summaryStats.methods} methods, " + + "${summaryStats.transitions} transitions" + } + + val statementRules = result.getOrPut(vulnerability.statement) { linkedMapOf() } + vulnerability.vulnerabilityRules.keys.forEach { rule -> + statementRules.getOrPut(rule, ::linkedSetOf) + } + } + + val sources = result.values.sumOf { statementRules -> statementRules.count { it.value.isNotEmpty() } } + val sinks = result.values.sumOf { statementRules -> statementRules.count { it.value.isEmpty() } } + logger.info { + "Forward actionable rule fallback: $sources relevant source rules, $sinks uncovered sinks" + } + return result + } + private fun fullScan( analysisStart: TimeSource.Monotonic.ValueTimeMark, entryPoints: List, startMethods: List, + actionableRules: List, ): Pair, Status> { - analysisManager.selectPhase(TaintAnalysisManager.Phase.FullScan) - ifdsEngine.resetApManager(apManager) + val fullScanManager = apManager + analysisManager.selectPhase( + TaintAnalysisManager.Phase.FullScan(mergeActionableRules(actionableRules)) + ) + ifdsEngine.resetApManager(fullScanManager) val analysisTimeout = (options.ifdsTimeout - analysisStart.elapsedNow()) * 0.80 runCatching { ifdsEngine.runAnalysis(startMethods, timeout = analysisTimeout, cancellationTimeout = 30.seconds) } @@ -206,7 +407,7 @@ abstract class TaintAnalyzer( return emptyList() to status } - val vulnerabilitiesWithTraces = ifdsEngine.generateTraces(entryPoints, vulnerabilities, traceResolutionTimeout) + val vulnerabilitiesWithTraces = ifdsEngine.generateTraces(fullScanManager, entryPoints, vulnerabilities, traceResolutionTimeout) .also { logger.info { "Finish trace generation" } } val filteredVulnerabilities = vulnerabilitiesWithTraces.filter { @@ -230,11 +431,44 @@ abstract class TaintAnalyzer( } } + private fun TaintAnalysisUnitRunnerManager.resolveActionableRules( + manager: ApManager, + entryPoints: List, + vulnerabilities: List, + timeout: Duration, + ): List { + (manager as? BaseOnlyApManager)?.enableTraceResolutionMode() + + val entryPointsSet = entryPoints.toHashSet() + val exactTimeBudget = + ExactProcessingTimeBudget(shallowRuleSearchExactTimeLimit) + val interProcTraces = resolveVulnerabilityInterProceduralTraces( + entryPointsSet, vulnerabilities, + resolverParams = TraceResolver.Params( + resolveEntryPointToStartTrace = false, + resolveAllTraces = true, + ), + timeout = timeout * 0.5, + cancellationTimeout = 30.seconds, + exactTimeBudget = exactTimeBudget, + ) + + return resolveVulnerabilityActionableRules( + interProcTraces, + timeout = timeout * 0.5, + cancellationTimeout = 30.seconds, + exactTimeBudget = exactTimeBudget, + ) + } + private fun TaintAnalysisUnitRunnerManager.generateTraces( + manager: ApManager, entryPoints: List, vulnerabilities: List, timeout: Duration, ): List { + (manager as? BaseOnlyApManager)?.enableTraceResolutionMode() + val entryPointsSet = entryPoints.toHashSet() val interProcTraces = resolveVulnerabilityInterProceduralTraces( entryPointsSet, vulnerabilities, @@ -354,6 +588,7 @@ abstract class TaintAnalyzer( } companion object { + private val shallowRuleSearchExactTimeLimit = 10.seconds private val logger = object : KLogging() {}.logger } } diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt index 7677c7e72..ee5073943 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt @@ -6,6 +6,7 @@ import kotlin.time.Duration data class TaintAnalyzerOptions( val ifdsTimeout: Duration, val ifdsApMode: ApMode, + val shallowScanApMode: ApMode = ApMode.BaseOnlyField, val symbolicExecutionEnabled: Boolean = false, val analysisCwe: Set? = null, val storeSummaries: Boolean = false, diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintRuleMarkFlow.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintRuleMarkFlow.kt new file mode 100644 index 000000000..899436db8 --- /dev/null +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintRuleMarkFlow.kt @@ -0,0 +1,66 @@ +package org.opentaint.common.sast.dataflow + +import org.opentaint.dataflow.configuration.CommonCondition +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.jvm.AssignMark +import org.opentaint.dataflow.configuration.jvm.Condition +import org.opentaint.dataflow.configuration.jvm.ContainsMark +import org.opentaint.dataflow.configuration.jvm.CopyAllMarks +import org.opentaint.dataflow.configuration.jvm.CopyMark +import org.opentaint.dataflow.configuration.jvm.RemoveAllMarks +import org.opentaint.dataflow.configuration.jvm.RemoveMark +import org.opentaint.dataflow.configuration.jvm.TaintCleaner +import org.opentaint.dataflow.configuration.jvm.TaintConfigurationSink +import org.opentaint.dataflow.configuration.jvm.TaintConfigurationSource +import org.opentaint.dataflow.configuration.jvm.TaintPassThrough + +internal data class TaintRuleMarkFlow( + val inputMarks: Set, + val outputMarks: Set, + val outputMarksComplete: Boolean, +) + +internal fun CommonTaintConfigurationItem.taintRuleMarkFlow( + actions: Set, +): TaintRuleMarkFlow { + val condition = when (this) { + is TaintConfigurationSource -> condition + is TaintConfigurationSink -> condition + is TaintPassThrough -> condition + is TaintCleaner -> condition + else -> null + } + val outputMarks = hashSetOf() + var outputMarksComplete = true + actions.forEach { action -> + when (action) { + is AssignMark -> outputMarks += action.mark.name + is CopyMark -> outputMarks += action.mark.name + is RemoveMark, is RemoveAllMarks -> Unit + is CopyAllMarks -> outputMarksComplete = false + else -> outputMarksComplete = false + } + } + return TaintRuleMarkFlow( + inputMarks = condition?.taintMarks().orEmpty(), + outputMarks = outputMarks, + outputMarksComplete = outputMarksComplete, + ) +} + +private fun Condition.taintMarks(): Set = buildSet { + fun collect(condition: CommonCondition<*>) { + when (condition) { + CommonCondition.True -> Unit + is CommonCondition.Atom<*> -> { + val atom = condition.atom + if (atom is ContainsMark) add(atom.mark.name) + } + is CommonCondition.Not<*> -> collect(condition.arg) + is CommonCondition.And<*> -> condition.args.forEach(::collect) + is CommonCondition.Or<*> -> condition.args.forEach(::collect) + } + } + collect(this@taintMarks) +} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JExplicitExceptionsOnlyApplicationGraph.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JExplicitExceptionsOnlyApplicationGraph.kt deleted file mode 100644 index 08af9ed84..000000000 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JExplicitExceptionsOnlyApplicationGraph.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.opentaint.jvm.sast.dataflow - -import org.opentaint.ir.api.jvm.JIRMethod -import org.opentaint.ir.api.jvm.cfg.JIRCatchInst -import org.opentaint.ir.api.jvm.cfg.JIRInst -import org.opentaint.ir.api.jvm.cfg.JIRThrowInst -import org.opentaint.ir.api.jvm.ext.cfg.callExpr -import org.opentaint.jvm.graph.JApplicationGraph -import org.opentaint.util.analysis.ApplicationGraph - -class JExplicitExceptionsOnlyApplicationGraph( - private val graph: JApplicationGraph -) : JApplicationGraph by graph { - class CutMethodGraph( - override val applicationGraph: JExplicitExceptionsOnlyApplicationGraph, - private val graph: ApplicationGraph.MethodGraph - ) : ApplicationGraph.MethodGraph by graph { - override fun successors(node: JIRInst): Sequence { - val flowGraph = node.location.method.flowGraph() - val successors = flowGraph.successors(node) - val catchers = if (isThrower(node)) flowGraph.catchers(node) else emptySet() - return successors.asSequence() + catchers.asSequence() - } - - override fun predecessors(node: JIRInst): Sequence { - val graph = node.location.method.flowGraph() - val predecessors = graph.predecessors(node) - val throwers = if (node is JIRCatchInst) graph.throwers(node).filter(::isThrower) else emptyList() - return predecessors.asSequence() + throwers.asSequence() - } - - private fun isThrower(node: JIRInst) = node is JIRThrowInst || node.callExpr != null - } - - override fun methodGraph(method: JIRMethod) = CutMethodGraph(this, graph.methodGraph(method)) -} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRCombinedTaintRulesProvider.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRCombinedTaintRulesProvider.kt index a9342238b..831661b8b 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRCombinedTaintRulesProvider.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRCombinedTaintRulesProvider.kt @@ -96,4 +96,15 @@ class JIRCombinedTaintRulesProvider( base.selectRules(ruleIds) combined.selectRules(ruleIds) } + + override fun relevantRuleIds(candidateRuleIds: Set): Set? { + val relevantRuleIds = listOfNotNull( + base.relevantRuleIds(candidateRuleIds), + combined.relevantRuleIds(candidateRuleIds), + ) + if (relevantRuleIds.isEmpty()) return null + return buildSet { + relevantRuleIds.forEach(::addAll) + } + } } diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRTaintAnalyzer.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRTaintAnalyzer.kt index 39a049f5b..0c04b1891 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRTaintAnalyzer.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JIRTaintAnalyzer.kt @@ -37,8 +37,8 @@ class JIRTaintAnalyzer( override fun analysisGraph(): ApplicationGraph { val usages = runBlocking { cp.usagesExt() } val mainGraph = JApplicationGraphImpl(cp, usages) - val explicitExceptionsOnlyGraph = JExplicitExceptionsOnlyApplicationGraph(mainGraph) - return JIRSafeApplicationGraph(explicitExceptionsOnlyGraph) + val tryBoundaryExceptionsGraph = JTryBoundaryExceptionsApplicationGraph(mainGraph) + return JIRSafeApplicationGraph(tryBoundaryExceptionsGraph) } private val analysisParams get() = JIRAnalysisManager.Params( @@ -85,4 +85,4 @@ class JIRTaintAnalyzer( !projectLocations.isProjectLocation(loc) } } -} \ No newline at end of file +} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JTryBoundaryExceptionsApplicationGraph.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JTryBoundaryExceptionsApplicationGraph.kt new file mode 100644 index 000000000..b5cdd89d2 --- /dev/null +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/JTryBoundaryExceptionsApplicationGraph.kt @@ -0,0 +1,56 @@ +package org.opentaint.jvm.sast.dataflow + +import org.opentaint.ir.api.jvm.JIRMethod +import org.opentaint.ir.api.jvm.cfg.JIRCatchInst +import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRThrowInst +import org.opentaint.jvm.graph.JApplicationGraph +import org.opentaint.util.analysis.ApplicationGraph + +class JTryBoundaryExceptionsApplicationGraph( + private val graph: JApplicationGraph +) : JApplicationGraph by graph { + class CutMethodGraph( + override val applicationGraph: JTryBoundaryExceptionsApplicationGraph, + private val graph: ApplicationGraph.MethodGraph + ) : ApplicationGraph.MethodGraph by graph { + private val flowGraph = graph.method.flowGraph() + + private val exceptionSourcesByCatcher: Map> by lazy { + graph.statements() + .filterIsInstance() + .associateWith(::selectExceptionSources) + } + + private val exceptionCatchersBySource: Map> by lazy { + val catchersBySource = hashMapOf>() + exceptionSourcesByCatcher.forEach { (catcher, sources) -> + sources.forEach { source -> + catchersBySource.getOrPut(source, ::hashSetOf).add(catcher) + } + } + catchersBySource + } + + override fun successors(node: JIRInst): Sequence { + return (flowGraph.successors(node) + exceptionCatchersBySource[node].orEmpty()).asSequence() + } + + override fun predecessors(node: JIRInst): Sequence { + return (flowGraph.predecessors(node) + exceptionSourcesByCatcher[node].orEmpty()).asSequence() + } + + private fun selectExceptionSources(catcher: JIRCatchInst): Set { + val protectedStatements = flowGraph.throwers(catcher) + val tryExits = protectedStatements.filter { statement -> + flowGraph.successors(statement).any { successor -> successor !in protectedStatements } + } + return buildSet { + protectedStatements.filterTo(this) { it is JIRThrowInst } + addAll(tryExits) + } + } + } + + override fun methodGraph(method: JIRMethod) = CutMethodGraph(this, graph.methodGraph(method)) +} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodClassTaintRulesStorage.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodClassTaintRulesStorage.kt index b19390a6f..f6655bf74 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodClassTaintRulesStorage.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodClassTaintRulesStorage.kt @@ -10,42 +10,29 @@ import org.opentaint.dataflow.jvm.util.JIRHierarchyInfo import org.opentaint.ir.api.jvm.JIRClassOrInterface import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.ext.allSuperHierarchy -import java.util.LinkedList -import java.util.Queue +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +private data class ResolvedMethodRules( + val storage: MethodClassTaintRulesStorage?, +) class MethodTaintRulesStorage private constructor( private val patternManager: PatternManager, private val hierarchyInfo: JIRHierarchyInfo, - private val concreteMethodNameRules: MutableMap>, + private val methodNameRules: ConcurrentHashMap>, private val patternMethodRules: Map>, private val anyMethodRules: MethodClassTaintRulesStorage?, ) { - private val methodNameWithoutConcreteRules = hashSetOf() - fun findRules(rules: MutableList, method: JIRMethod) { anyMethodRules?.findRules(rules, method) - val concreteRules = concreteMethodNameRules[method.name] - if (concreteRules != null) { - concreteRules.findRules(rules, method) - return - } - - if (method.name in methodNameWithoutConcreteRules) { - return - } - - val builder = MethodClassTaintRulesStorage.Builder(patternManager, hierarchyInfo, method.name) - resolvePatterns(patternMethodRules, method.name, builder) - val storage = builder.build() - - if (storage == null) { - methodNameWithoutConcreteRules.add(method.name) - return + val resolved = methodNameRules.computeIfAbsent(method.name) { methodName -> + val builder = MethodClassTaintRulesStorage.Builder(patternManager, hierarchyInfo, methodName) + resolvePatterns(patternMethodRules, methodName, builder) + ResolvedMethodRules(builder.build()) } - - concreteMethodNameRules[method.name] = storage - storage.findRules(rules, method) + resolved.storage?.findRules(rules, method) } class Builder( @@ -86,10 +73,10 @@ class MethodTaintRulesStorage private constructor( .mapValuesTo(hashMapOf()) { it.value.toTypedArray() } - val concreteRules = hashMapOf>() + val concreteRules = ConcurrentHashMap>() for ((methodName, builder) in concreteMethodNameRules) { resolvePatterns(compiledPatternMethodRules, methodName, builder) - concreteRules[methodName] = builder.build() ?: continue + concreteRules[methodName] = ResolvedMethodRules(builder.build()) } return MethodTaintRulesStorage( @@ -125,15 +112,20 @@ private class MethodClassTaintRulesStorage private construct private val concreteMethodName: String?, private val patterns: ClassNamePattern, private val anyRules: Array, - private val concreteClassRules: MutableMap>, + initialConcreteClassRules: Map>, ) { - private val patternResolvedClasses = hashSetOf() - private val pushDelayRulesQueue: Queue>> = LinkedList() + private val concreteClassRules = ConcurrentHashMap>() + private val resolvedPatternRules = ConcurrentHashMap>() + private val pushDelayRulesQueue = ConcurrentLinkedQueue>>() init { - for ((className, rules) in concreteClassRules) { + for ((className, rules) in initialConcreteClassRules) { + val concurrentRules = ConcurrentHashMap.newKeySet() + concurrentRules.addAll(rules) + this.concreteClassRules[className] = concurrentRules registerRules(className, rules) } + pushDelayedRules() } private fun registerRules(className: String, rules: Iterable) { @@ -142,12 +134,8 @@ private class MethodClassTaintRulesStorage private construct } private fun pushDelayedRules() { - if (pushDelayRulesQueue.isEmpty()) return - - val iter = pushDelayRulesQueue.iterator() - while (iter.hasNext()) { - val (className, rules) = iter.next() - iter.remove() + while (true) { + val (className, rules) = pushDelayRulesQueue.poll() ?: return val cls = hierarchyInfo.cp.findClassOrNull(className) ?: continue pushRuleForSuperTypes(cls, rules) @@ -169,7 +157,7 @@ private class MethodClassTaintRulesStorage private construct cls.allSuperHierarchy.filter { c -> c.declaredMethods.any { it.name == concreteMethodName } }.forEach { c -> - concreteClassRules.getOrPut(c.name, ::hashSetOf).addAll(conditionedRules) + concreteClassRules.computeIfAbsent(c.name) { ConcurrentHashMap.newKeySet() }.addAll(conditionedRules) } } @@ -207,23 +195,21 @@ private class MethodClassTaintRulesStorage private construct } } - if (!patternResolvedClasses.add(className)) { - return - } + val newRules = resolvedPatternRules.computeIfAbsent(className) { + val resolved = hashSetOf() + resolveClassNamePattern(patterns, className, resolved) - val newRules = hashSetOf() - resolveClassNamePattern(patterns, className, newRules) + if (innerClassNameWithDots != null) { + resolveClassNamePattern(patterns, innerClassNameWithDots, resolved) + } - if (innerClassNameWithDots != null) { - resolveClassNamePattern(patterns, innerClassNameWithDots, newRules) + if (resolved.isNotEmpty()) { + registerRules(className, resolved) + pushDelayedRules() + concreteClassRules.computeIfAbsent(className) { ConcurrentHashMap.newKeySet() }.addAll(resolved) + } + resolved } - - if (newRules.isEmpty()) return - - registerRules(className, newRules) - pushDelayedRules() - - concreteClassRules.getOrPut(className, ::hashSetOf).addAll(newRules) dst.addAll(newRules) return diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt index 27a16e8c0..66d28fa27 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt @@ -85,7 +85,8 @@ class MethodTaintConfigurationResolver( val taintMarkManager: TaintMarkManager, val cp: JIRClasspath, val objectTypeName: TypeName, - val method: JIRMethod + val method: JIRMethod, + private val interner: ResolvedRuleInterner = ResolvedRuleInterner(), ) { private val typedMethod by lazy { resolveTypedMethod() } @@ -176,7 +177,7 @@ class MethodTaintConfigurationResolver( val condition = resolveCondition(serializedCondition, it).simplify() if (condition.isFalse()) return@mapNotNull null - resolveMethodRule(condition, it) + resolveMethodRule(interner.internCondition(condition), it) } } @@ -185,50 +186,55 @@ class MethodTaintConfigurationResolver( ctx: AnyArgSpecializationCtx, ): TaintConfigurationItem = when (this) { is SerializedRule.EntryPoint -> { - TaintEntryPointSource(method, condition, taint.flatMap { it.resolveWithArray(ctx) }, info, serializedId) + TaintEntryPointSource(method, condition, assignActions(taint, ctx), info, serializedId) } is SerializedRule.Source -> { - TaintMethodSource(method, condition, taint.flatMap { it.resolveWithArray(ctx) }, info, serializedId) + TaintMethodSource(method, condition, assignActions(taint, ctx), info, serializedId) } is SerializedRule.MethodExitSource -> { - TaintMethodExitSource(method, condition, taint.flatMap { it.resolveWithArray(ctx) }, info, serializedId) + TaintMethodExitSource(method, condition, assignActions(taint, ctx), info, serializedId) } is SerializedRule.Sink -> { TaintMethodSink( - method, condition, - trackFactsReachAnalysisEnd?.flatMap { it.resolveNoArray(ctx) }.orEmpty(), + method, condition, trackedFacts(ctx), ruleId(), meta(), info, serializedId ) } is SerializedRule.MethodExitSink -> { TaintMethodExitSink( - method, condition, - trackFactsReachAnalysisEnd?.flatMap { it.resolveNoArray(ctx) }.orEmpty(), + method, condition, trackedFacts(ctx), ruleId(), meta(), info, serializedId ) } is SerializedRule.MethodEntrySink -> { TaintMethodEntrySink( - method, condition, - trackFactsReachAnalysisEnd?.flatMap { it.resolveNoArray(ctx) }.orEmpty(), + method, condition, trackedFacts(ctx), ruleId(), meta(), info, serializedId ) } is SerializedRule.PassThrough -> { - TaintPassThrough(method, condition, copy.flatMap { it.resolve(ctx) }, info, serializedId) + TaintPassThrough(method, condition, interner.internList(copy.flatMap { it.resolve(ctx) }), info, serializedId) } is SerializedRule.Cleaner -> { - TaintCleaner(method, condition, cleans.flatMap { it.resolve(ctx) }, info, serializedId) + TaintCleaner(method, condition, interner.internList(cleans.flatMap { it.resolve(ctx) }), info, serializedId) } } + private fun assignActions( + actions: List, + ctx: AnyArgSpecializationCtx, + ): List = interner.internList(actions.flatMap { it.resolveWithArray(ctx) }) + + private fun SinkRule.trackedFacts(ctx: AnyArgSpecializationCtx): List = + interner.internList(trackFactsReachAnalysisEnd?.flatMap { it.resolveNoArray(ctx) }.orEmpty()) + private val ruleIdGen = AtomicInteger() private fun SinkRule.ruleId(): String { @@ -237,10 +243,12 @@ class MethodTaintConfigurationResolver( return "generated-id-${ruleIdGen.incrementAndGet()}" } - private fun SinkRule.meta(): TaintSinkMeta = TaintSinkMeta( - message = meta?.message() ?: "", - severity = meta?.severity ?: CommonTaintConfigurationSinkMeta.Severity.Warning, - cwe = meta?.cwe + private fun SinkRule.meta(): TaintSinkMeta = interner.intern( + TaintSinkMeta( + message = meta?.message() ?: "", + severity = meta?.severity ?: CommonTaintConfigurationSinkMeta.Severity.Warning, + cwe = meta?.cwe + ) ) private fun SinkMetaData.message(): String? = note diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/PatternManager.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/PatternManager.kt index 57f570774..4f9e03267 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/PatternManager.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/PatternManager.kt @@ -1,10 +1,12 @@ package org.opentaint.jvm.sast.dataflow.rules +import java.util.concurrent.ConcurrentHashMap + class PatternManager { - private val compiledMatchers = hashMapOf() + private val compiledMatchers = ConcurrentHashMap() fun compilePattern(pattern: String): Regex = - compiledMatchers.getOrPut(pattern) { pattern.toRegex() } + compiledMatchers.computeIfAbsent(pattern) { it.toRegex() } fun matchPattern(pattern: String, str: String): Boolean = compilePattern(pattern).containsMatchIn(str) diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/ResolvedRuleInterner.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/ResolvedRuleInterner.kt new file mode 100644 index 000000000..e73f4b14b --- /dev/null +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/ResolvedRuleInterner.kt @@ -0,0 +1,36 @@ +package org.opentaint.jvm.sast.dataflow.rules + +import org.opentaint.dataflow.configuration.CommonCondition +import org.opentaint.dataflow.configuration.jvm.Condition +import java.util.concurrent.ConcurrentHashMap + +class ResolvedRuleInterner { + private val values = ConcurrentHashMap() + + @Suppress("UNCHECKED_CAST") + fun intern(value: T): T { + values[value]?.let { return it as T } + return (values.putIfAbsent(value, value) ?: value) as T + } + + fun internList(list: List): List { + if (list.isEmpty()) return emptyList() + return intern(list.mapTo(ArrayList(list.size)) { intern(it) }) + } + + fun internCondition(condition: Condition): Condition = when (condition) { + is CommonCondition.True -> condition + + is CommonCondition.Atom -> intern(CommonCondition.Atom(intern(condition.atom))) + + is CommonCondition.Not -> intern(CommonCondition.Not(internCondition(condition.arg))) + + is CommonCondition.And -> intern( + CommonCondition.And(internList(condition.args.map { internCondition(it) })) + ) + + is CommonCondition.Or -> intern( + CommonCondition.Or(internList(condition.args.map { internCondition(it) })) + ) + } +} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt index a2d7c5bf2..7db97f1ea 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt @@ -31,10 +31,12 @@ import org.opentaint.ir.api.jvm.ext.allSuperHierarchySequence import org.opentaint.ir.api.jvm.ext.objectClass import org.opentaint.ir.impl.util.adjustEmptyList import org.opentaint.jvm.util.typename +import java.util.concurrent.ConcurrentHashMap class TaintConfiguration(private val cp: JIRClasspath) { private val patternManager = PatternManager() private val taintMarkManager = TaintMarkManager() + private val interner = ResolvedRuleInterner() private val hierarchyInfo = JIRHierarchyInfo(cp) private val objectTypeName = cp.objectClass.typename @@ -117,24 +119,28 @@ class TaintConfiguration(private val cp: JIRClasspath) { private inner class TaintRulesStorage { private var builder: MethodTaintRulesStorage.Builder? = MethodTaintRulesStorage.Builder(patternManager, hierarchyInfo) + @Volatile private var storage: MethodTaintRulesStorage? = null private fun storage(): MethodTaintRulesStorage { storage?.let { return it } - storage = builder?.build() - builder = null - - return storage ?: error("Storage initialization failed") + return synchronized(this) { + storage ?: builder?.build()?.also { + storage = it + builder = null + } ?: error("Storage initialization failed") + } } + @Synchronized fun addRules(rules: List) { val builder = this.builder ?: error("Storage rule set closed") builder.addRules(rules) } - private val methodItems = hashMapOf>() - private val methodAllRelevantItems = hashMapOf>() + private val methodItems = ConcurrentHashMap>() + private val methodAllRelevantItems = ConcurrentHashMap>() fun configForMethod(method: JIRMethod, allRelevant: Boolean): List = if (!allRelevant) { getConfigForMethod(method) @@ -142,13 +148,11 @@ class TaintConfiguration(private val cp: JIRClasspath) { getAllRelevantConfigForMethod(method) } - @Synchronized - private fun getConfigForMethod(method: JIRMethod): List = methodItems.getOrPut(method) { + private fun getConfigForMethod(method: JIRMethod): List = methodItems.computeIfAbsent(method) { resolveMethodItems(method).adjustEmptyList() } - @Synchronized - private fun getAllRelevantConfigForMethod(method: JIRMethod): List = methodAllRelevantItems.getOrPut(method) { + private fun getAllRelevantConfigForMethod(method: JIRMethod): List = methodAllRelevantItems.computeIfAbsent(method) { resolveMethodRelevantItems(method).adjustEmptyList() } @@ -169,7 +173,8 @@ class TaintConfiguration(private val cp: JIRClasspath) { rules.removeAll { !it.function.matchFunctionName(method) } if (rules.isEmpty()) return emptyList() - val resolver = MethodTaintConfigurationResolver(patternManager, taintMarkManager, cp, objectTypeName, method) + val resolver = + MethodTaintConfigurationResolver(patternManager, taintMarkManager, cp, objectTypeName, method, interner) rules.removeAll { with(resolver) { it.signature?.matchFunctionSignature() == false @@ -211,7 +216,7 @@ class TaintConfiguration(private val cp: JIRClasspath) { } actions += AssignMark(taintMarkManager.taintMark(action.kind), Result) } - return listOf(TaintStaticFieldSource(field, mkTrue(), actions, info, serializedId)) + return listOf(TaintStaticFieldSource(field, mkTrue(), interner.internList(actions), info, serializedId)) } } } diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintMarkManager.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintMarkManager.kt index faf84044b..f809b9297 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintMarkManager.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintMarkManager.kt @@ -1,9 +1,10 @@ package org.opentaint.jvm.sast.dataflow.rules import org.opentaint.dataflow.configuration.jvm.TaintMark +import java.util.concurrent.ConcurrentHashMap class TaintMarkManager { - private val taintMarks = hashMapOf() + private val taintMarks = ConcurrentHashMap() - fun taintMark(name: String): TaintMark = taintMarks.getOrPut(name) { TaintMark(name) } + fun taintMark(name: String): TaintMark = taintMarks.computeIfAbsent(name) { TaintMark(it) } } diff --git a/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/common/sast/dataflow/HybridActionableRuleSelectionTest.kt b/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/common/sast/dataflow/HybridActionableRuleSelectionTest.kt new file mode 100644 index 000000000..abe72e117 --- /dev/null +++ b/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/common/sast/dataflow/HybridActionableRuleSelectionTest.kt @@ -0,0 +1,56 @@ +package org.opentaint.common.sast.dataflow + +import org.junit.jupiter.api.Test +import org.opentaint.dataflow.ap.ifds.trace.action.ActionableRulesCollectionResult +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class HybridActionableRuleSelectionTest { + @Test + fun `does not request fallback when every vulnerability is covered`() { + var fallbackCalled = false + val exact = ActionableRulesCollectionResult.Collected(emptyMap()) + + val result = actionableRulesWithFallback(listOf(exact, exact)) { + fallbackCalled = true + null + } + + assertEquals(listOf(exact, exact), result) + assertFalse(fallbackCalled) + } + + @Test + fun `keeps exact results and adds one fallback for unprocessed vulnerabilities`() { + val first = ActionableRulesCollectionResult.Collected(emptyMap()) + val fallback = ActionableRulesCollectionResult.Collected(emptyMap()) + var unprocessedIndices: List? = null + + val result = actionableRulesWithFallback( + listOf( + first, + ActionableRulesCollectionResult.Unprocessed, + ActionableRulesCollectionResult.Unprocessed, + ) + ) { indices -> + unprocessedIndices = indices + fallback + } + + assertEquals(listOf(1, 2), unprocessedIndices) + assertEquals(listOf(first, fallback), result) + } + + @Test + fun `does not fall back for a completed invalid trace`() { + var fallbackCalled = false + + val result = actionableRulesWithFallback(listOf(ActionableRulesCollectionResult.Failed)) { + fallbackCalled = true + ActionableRulesCollectionResult.Collected(emptyMap()) + } + + assertEquals(emptyList(), result) + assertFalse(fallbackCalled) + } +} diff --git a/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/common/sast/dataflow/TaintRuleMarkFlowTest.kt b/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/common/sast/dataflow/TaintRuleMarkFlowTest.kt new file mode 100644 index 000000000..00548ebd9 --- /dev/null +++ b/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/common/sast/dataflow/TaintRuleMarkFlowTest.kt @@ -0,0 +1,54 @@ +package org.opentaint.common.sast.dataflow + +import org.opentaint.dataflow.configuration.CommonCondition +import org.opentaint.dataflow.configuration.jvm.Argument +import org.opentaint.dataflow.configuration.jvm.AssignMark +import org.opentaint.dataflow.configuration.jvm.ContainsMark +import org.opentaint.dataflow.configuration.jvm.Result +import org.opentaint.dataflow.configuration.jvm.TaintMark +import org.opentaint.dataflow.configuration.jvm.TaintMethodSource +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class TaintRuleMarkFlowTest { + @Test + fun `extracts condition and assigned marks`() { + val input = TaintMark("input") + val output = TaintMark("output") + val action = AssignMark(output, Result) + val rule = TaintMethodSource( + method = TestMethod, + condition = CommonCondition.Atom(ContainsMark(Argument(0), input)), + actionsAfter = listOf(action), + info = null, + ) + + val flow = rule.taintRuleMarkFlow(setOf(action)) + + assertEquals(setOf("input"), flow.inputMarks) + assertEquals(setOf("output"), flow.outputMarks) + assertTrue(flow.outputMarksComplete) + } + + private data object TestMethod : CommonMethod { + override val name: String = "test" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = object : ControlFlowGraph { + override val instructions: List = emptyList() + override val entries: List = emptyList() + override val exits: List = emptyList() + override fun successors(node: CommonInst): Set = emptySet() + override fun predecessors(node: CommonInst): Set = emptySet() + } + } +} diff --git a/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/jvm/sast/dataflow/rules/PatternManagerTest.kt b/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/jvm/sast/dataflow/rules/PatternManagerTest.kt new file mode 100644 index 000000000..6e91affdf --- /dev/null +++ b/core/opentaint-jvm-sast-dataflow/src/test/kotlin/org/opentaint/jvm/sast/dataflow/rules/PatternManagerTest.kt @@ -0,0 +1,24 @@ +package org.opentaint.jvm.sast.dataflow.rules + +import java.util.concurrent.Executors +import kotlin.test.Test +import kotlin.test.assertSame + +class PatternManagerTest { + @Test + fun `compiled patterns are shared between concurrent callers`() { + val manager = PatternManager() + val executor = Executors.newFixedThreadPool(8) + + try { + val patterns = (0 until 1_000).map { + executor.submit { manager.compilePattern("foo.*bar") } + }.map { it.get() } + + val expected = patterns.first() + patterns.forEach { assertSame(expected, it) } + } finally { + executor.shutdownNow() + } + } +} diff --git a/core/samples-dependency/build.gradle.kts b/core/samples-dependency/build.gradle.kts new file mode 100644 index 000000000..fcd778c82 --- /dev/null +++ b/core/samples-dependency/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + java +} + +tasks.withType { + sourceCompatibility = JavaVersion.VERSION_1_8.toString() + targetCompatibility = JavaVersion.VERSION_1_8.toString() + options.compilerArgs.add("-g") +} diff --git a/core/samples-dependency/src/main/java/org/springframework/beans/factory/annotation/Autowired.java b/core/samples-dependency/src/main/java/org/springframework/beans/factory/annotation/Autowired.java new file mode 100644 index 000000000..9e9101a6b --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/beans/factory/annotation/Autowired.java @@ -0,0 +1,10 @@ +package org.springframework.beans.factory.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.CONSTRUCTOR}) +public @interface Autowired { } diff --git a/core/samples-dependency/src/main/java/org/springframework/data/repository/Repository.java b/core/samples-dependency/src/main/java/org/springframework/data/repository/Repository.java new file mode 100644 index 000000000..7070fdaec --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/data/repository/Repository.java @@ -0,0 +1,4 @@ +package org.springframework.data.repository; + +public interface Repository { +} diff --git a/core/samples-dependency/src/main/java/org/springframework/http/HttpEntity.java b/core/samples-dependency/src/main/java/org/springframework/http/HttpEntity.java new file mode 100644 index 000000000..9362c1f2f --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/http/HttpEntity.java @@ -0,0 +1,11 @@ +package org.springframework.http; + +public class HttpEntity { + public T Body; + + public HttpEntity() { } + + public HttpEntity(T body) { + this.Body = body; + } +} diff --git a/core/samples-dependency/src/main/java/org/springframework/http/HttpHeaders.java b/core/samples-dependency/src/main/java/org/springframework/http/HttpHeaders.java new file mode 100644 index 000000000..0200cfc30 --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/http/HttpHeaders.java @@ -0,0 +1,7 @@ +package org.springframework.http; + +public class HttpHeaders { + public void setContentType(MediaType mediaType) { } + public void setContentLength(long length) { } + public void setContentDispositionFormData(String disposition, String filename) { } +} diff --git a/core/samples-dependency/src/main/java/org/springframework/http/HttpStatus.java b/core/samples-dependency/src/main/java/org/springframework/http/HttpStatus.java new file mode 100644 index 000000000..eb82e92b5 --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/http/HttpStatus.java @@ -0,0 +1,5 @@ +package org.springframework.http; + +public enum HttpStatus { + OK +} diff --git a/core/samples-dependency/src/main/java/org/springframework/http/MediaType.java b/core/samples-dependency/src/main/java/org/springframework/http/MediaType.java new file mode 100644 index 000000000..2c99e6374 --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/http/MediaType.java @@ -0,0 +1,5 @@ +package org.springframework.http; + +public class MediaType { + public static final MediaType APPLICATION_JSON = new MediaType(); +} diff --git a/core/samples-dependency/src/main/java/org/springframework/http/ResponseEntity.java b/core/samples-dependency/src/main/java/org/springframework/http/ResponseEntity.java new file mode 100644 index 000000000..126e6a4a2 --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/http/ResponseEntity.java @@ -0,0 +1,12 @@ +package org.springframework.http; + +public class ResponseEntity extends HttpEntity { + + public ResponseEntity() { } + + public ResponseEntity(T body) { super(body); } + + public ResponseEntity(T body, HttpHeaders headers, HttpStatus status) { + super(body); + } +} diff --git a/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/GetMapping.java b/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/GetMapping.java new file mode 100644 index 000000000..d0d32d535 --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/GetMapping.java @@ -0,0 +1,10 @@ +package org.springframework.web.bind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface GetMapping { } diff --git a/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java new file mode 100644 index 000000000..ded722e32 --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java @@ -0,0 +1,3 @@ +package org.springframework.web.bind.annotation; + +public @interface ModelAttribute { } diff --git a/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/RestController.java b/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/RestController.java new file mode 100644 index 000000000..735ecca2e --- /dev/null +++ b/core/samples-dependency/src/main/java/org/springframework/web/bind/annotation/RestController.java @@ -0,0 +1,10 @@ +package org.springframework.web.bind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface RestController { } diff --git a/core/samples-dependency/src/main/java/stirling/external/StirlingExternal.java b/core/samples-dependency/src/main/java/stirling/external/StirlingExternal.java new file mode 100644 index 000000000..5ed3abc10 --- /dev/null +++ b/core/samples-dependency/src/main/java/stirling/external/StirlingExternal.java @@ -0,0 +1,36 @@ +package stirling.external; + +public final class StirlingExternal { + private StirlingExternal() { } + + public static final class FileInput { + public String getSize() { return null; } + } + + public static final class PdfDocumentFactory { + public PdfDocument load(FileInput input, boolean readOnly) { return null; } + } + + public static final class PdfDocument { + public DocumentInfo getDocumentInformation() { return null; } + } + + public static final class DocumentInfo { + public String getTitle() { return null; } + } + + public static final class JsonMapper { + public JsonNode createObjectNode() { return null; } + public JsonWriter writerWithDefaultPrettyPrinter() { return null; } + } + + public static final class JsonNode { + public JsonNode put(String name, String value) { return this; } + public JsonNode put(String name, long value) { return this; } + public JsonNode set(String name, JsonNode value) { return this; } + } + + public static final class JsonWriter { + public String writeValueAsString(JsonNode value) { return null; } + } +} diff --git a/core/samples-dependency/src/main/java/test/library/LibraryFragment.java b/core/samples-dependency/src/main/java/test/library/LibraryFragment.java new file mode 100644 index 000000000..82ed0ee65 --- /dev/null +++ b/core/samples-dependency/src/main/java/test/library/LibraryFragment.java @@ -0,0 +1,7 @@ +package test.library; + +public interface LibraryFragment { + String libraryQuery(String criteria); + + String libraryLookup(String url); +} diff --git a/core/samples/build.gradle.kts b/core/samples/build.gradle.kts index ae7a2ab50..a97ef89fa 100644 --- a/core/samples/build.gradle.kts +++ b/core/samples/build.gradle.kts @@ -13,6 +13,7 @@ repositories { dependencies { implementation(kotlin("stdlib")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + implementation(project(":samples-dependency")) } tasks { diff --git a/core/samples/src/main/java/test/samples/BaseOnlyClassStaticFootprintSample.java b/core/samples/src/main/java/test/samples/BaseOnlyClassStaticFootprintSample.java new file mode 100644 index 000000000..cdc2fb088 --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyClassStaticFootprintSample.java @@ -0,0 +1,37 @@ +package test.samples; + +public class BaseOnlyClassStaticFootprintSample { + public static Object source() { + return new Object(); + } + + public static void seed(Object value) { + } + + public static void transition(Object value) { + } + + public static void sink(Object value) { + } + + private static void irrelevantLeaf() { + Object ignored = new Object(); + ignored.toString(); + } + + private static void irrelevantWrapper() { + irrelevantLeaf(); + } + + private static void relevantWrapper(Object value) { + transition(value); + } + + public static void transitiveRuleFootprint() { + Object value = source(); + seed(value); + irrelevantWrapper(); + relevantWrapper(value); + sink(value); + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyNestedReferenceRegressionSample.java b/core/samples/src/main/java/test/samples/BaseOnlyNestedReferenceRegressionSample.java new file mode 100644 index 000000000..a42696105 --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyNestedReferenceRegressionSample.java @@ -0,0 +1,28 @@ +package test.samples; + +public class BaseOnlyNestedReferenceRegressionSample { + private static String source() { return "tainted"; } + private static void sink(String value) { } + + public static void nestedReferenceFlow() { + Payload payload = new Payload(source()); + Envelope envelope = new Envelope(payload); + sink(envelope.payload.value); + } + + private static final class Payload { + private final String value; + + private Payload(String value) { + this.value = value; + } + } + + private static final class Envelope { + private final Payload payload; + + private Envelope(Payload payload) { + this.payload = payload; + } + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyReferenceInstallFuzzSample.java b/core/samples/src/main/java/test/samples/BaseOnlyReferenceInstallFuzzSample.java new file mode 100644 index 000000000..67fd3827c --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyReferenceInstallFuzzSample.java @@ -0,0 +1,231 @@ +package test.samples; + +public class BaseOnlyReferenceInstallFuzzSample { + private static String source() { return "tainted"; } + private static void sink(String value) { } + + private static String stringAlias(String value) { return value; } + private static Cell cellAlias(Cell value) { return value; } + + private static Cell makeCell(String value) { return new Cell(value); } + private static Cell makeTaggedCell(String value, Tag tag) { return new Cell(value, tag); } + private static Cell makeTaggedCell(Tag tag, String value) { return new Cell(tag, value); } + private static Cell makeCellViaAlias(String value) { return new Cell(stringAlias(value)); } + private static Cell makeCellNested(String value) { return makeCell(value); } + + private static void install(Cell cell, String value) { cell.set(value); } + private static void installTagged(Cell cell, Tag tag, String value) { cell.setTagged(tag, value); } + private static void installTagged(Cell cell, String value, Tag tag) { cell.setTagged(value, tag); } + private static void installNested(Cell cell, String value) { install(cell, value); } + private static Cell installAndReturn(Cell cell, String value) { cell.set(value); return cell; } + + public static void directConstructorInstall() { + Cell cell = new Cell(source()); + sink(cell.value); + } + + public static void constructorInstallFromLocal() { + String value = source(); + Cell cell = new Cell(value); + sink(cell.value); + } + + public static void constructorInstallFromAlias() { + String value = source(); + String alias = value; + Cell cell = new Cell(alias); + sink(cell.value); + } + + public static void constructorInstallFromTwoAliases() { + String value = source(); + String first = value; + String second = first; + Cell cell = new Cell(second); + sink(cell.value); + } + + public static void constructorInstallFromIdentity() { + Cell cell = new Cell(stringAlias(source())); + sink(cell.value); + } + + public static void constructorInstallFromReferenceCast() { + Object value = source(); + Cell cell = new Cell((String) value); + sink(cell.value); + } + + public static void constructorInstallFirstArgument() { + Cell cell = new Cell(source(), new Tag()); + sink(cell.value); + } + + public static void constructorInstallSecondArgument() { + Cell cell = new Cell(new Tag(), source()); + sink(cell.value); + } + + public static void directFactoryInstall() { + Cell cell = makeCell(source()); + sink(cell.value); + } + + public static void factoryInstallFromAlias() { + String value = source(); + String alias = value; + Cell cell = makeCell(alias); + sink(cell.value); + } + + public static void factoryInstallFromCast() { + Object value = source(); + Cell cell = makeCell((String) value); + sink(cell.value); + } + + public static void nestedFactoryInstall() { + Cell cell = makeCellNested(source()); + sink(cell.value); + } + + public static void factoryAliasInsideInstall() { + Cell cell = makeCellViaAlias(source()); + sink(cell.value); + } + + public static void factoryInstallFirstArgument() { + Cell cell = makeTaggedCell(source(), new Tag()); + sink(cell.value); + } + + public static void factoryInstallSecondArgument() { + Cell cell = makeTaggedCell(new Tag(), source()); + sink(cell.value); + } + + public static void directSetterInstall() { + Cell cell = new Cell(); + cell.set(source()); + sink(cell.value); + } + + public static void setterInstallFromAlias() { + String value = source(); + String alias = value; + Cell cell = new Cell(); + cell.set(alias); + sink(cell.value); + } + + public static void setterInstallFromCast() { + Object value = source(); + Cell cell = new Cell(); + cell.set((String) value); + sink(cell.value); + } + + public static void setterInstallThroughReceiverAlias() { + Cell cell = new Cell(); + Cell alias = cell; + alias.set(source()); + sink(cell.value); + } + + public static void setterInstallThroughReceiverIdentity() { + Cell cell = new Cell(); + cellAlias(cell).set(source()); + sink(cell.value); + } + + public static void helperSetterInstall() { + Cell cell = new Cell(); + install(cell, source()); + sink(cell.value); + } + + public static void nestedHelperSetterInstall() { + Cell cell = new Cell(); + installNested(cell, source()); + sink(cell.value); + } + + public static void helperSetterInstallLastArgument() { + Cell cell = new Cell(); + installTagged(cell, new Tag(), source()); + sink(cell.value); + } + + public static void helperSetterInstallMiddleArgument() { + Cell cell = new Cell(); + installTagged(cell, source(), new Tag()); + sink(cell.value); + } + + public static void helperReturnsInstalledWrapper() { + Cell cell = installAndReturn(new Cell(), source()); + sink(cell.value); + } + + public static void constructorInstallThroughEnvelope() { + Envelope envelope = new Envelope(new Cell(source())); + sink(envelope.cell.value); + } + + public static void constructorInstallThroughTwoEnvelopes() { + OuterEnvelope outer = new OuterEnvelope(new Envelope(new Cell(source()))); + sink(outer.envelope.cell.value); + } + + public static void setterInstallThroughEnvelope() { + Envelope envelope = new Envelope(new Cell()); + envelope.cell.set(source()); + sink(envelope.cell.value); + } + + public static void branchConstructorInstall() { + String value = source(); + Cell cell; + if (value != null) { + cell = new Cell(value); + } else { + cell = new Cell(); + } + sink(cell.value); + } + + public static void branchSetterInstall() { + String value = source(); + Cell cell = new Cell(); + if (value != null) { + cell.set(value); + } + sink(cell.value); + } + + private static final class Tag { } + + private static final class Cell { + private String value; + private Tag tag; + + Cell() { } + Cell(String value) { this.value = value; } + Cell(String value, Tag tag) { this.value = value; this.tag = tag; } + Cell(Tag tag, String value) { this.tag = tag; this.value = value; } + + void set(String value) { this.value = value; } + void setTagged(Tag tag, String value) { this.tag = tag; this.value = value; } + void setTagged(String value, Tag tag) { this.value = value; this.tag = tag; } + } + + private static final class Envelope { + private final Cell cell; + Envelope(Cell cell) { this.cell = cell; } + } + + private static final class OuterEnvelope { + private final Envelope envelope; + OuterEnvelope(Envelope envelope) { this.envelope = envelope; } + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyReferenceMutationFuzzSample.java b/core/samples/src/main/java/test/samples/BaseOnlyReferenceMutationFuzzSample.java new file mode 100644 index 000000000..d0d17c368 --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyReferenceMutationFuzzSample.java @@ -0,0 +1,646 @@ +package test.samples; + +public class BaseOnlyReferenceMutationFuzzSample { + private static String source() { return "tainted"; } + private static void sink(String value) { } + private static String identity(String value) { return value; } + private static Box alias(Box box) { return box; } + + private static void setLabel(Box box, String value) { box.setLabel(value); } + private static void setPeer(Box box, Peer value) { box.setPeer(value); } + private static void setBoth(Box box, String first, String second) { box.setMetadata(first, second); } + private static void setReferences(Box box, Peer peer, Node node) { box.setReferences(peer, node); } + private static void nestedSetLabel(Box box, String value) { setLabel(box, value); } + private static Box touchAndReturn(Box box, String value) { box.setLabel(value); return box; } + private static Holder makeHolder(Box box) { return new Holder(box); } + private static Holder makeHolder(Box box, String name) { return new Holder(box, name); } + private static Holder makeAliasedHolder(Box box) { return new Holder(alias(box)); } + private static Pair makePairFirst(Box box) { return new Pair(box, new Box()); } + private static Pair makePairSecond(Box box) { return new Pair(new Box(), box); } + private static Triple makeTripleFirst(Box box) { return new Triple(box, new Box(), new Box()); } + private static Quad makeQuadFirst(Box box) { return new Quad(box, new Box(), new Box(), new Box()); } + private static Quad makeQuadSecond(Box box) { return new Quad(new Box(), box, new Box(), new Box()); } + private static Quad makeQuadThird(Box box) { return new Quad(new Box(), new Box(), box, new Box()); } + private static Quad makeQuadFourth(Box box) { return new Quad(new Box(), new Box(), new Box(), box); } + private static void installBox(Holder holder, Box box) { holder.setBox(box); } + private static void installBox(Holder holder, Box box, String name) { holder.setBoxAndName(box, name); } + private static void installPrimary(AlternateHolder holder, Box box) { holder.setPrimary(box); } + private static void installSecondary(AlternateHolder holder, Box box) { holder.setSecondary(box); } + private static void nestedInstallPrimary(AlternateHolder holder, Box box) { installPrimary(holder, box); } + private static AlternateHolder makeAlternatePrimary(Box box) { return new AlternateHolder(box, new Box()); } + private static KeyedHolder makeKeyedLeft(Box box) { return new KeyedHolder(box, new Box(), new Box()); } + private static KeyedHolder makeKeyedCenter(Box box) { return new KeyedHolder(new Box(), box, new Box()); } + + public static void directStringMetadataSetter() { + Box box = new Box(); box.setPayload(source()); box.setLabel("safe"); sink(box.getPayload()); + } + + public static void directCategorySetter() { + Box box = new Box(); box.setPayload(source()); box.setCategory("safe"); sink(box.getPayload()); + } + + public static void directPeerSetter() { + Box box = new Box(); box.setPayload(source()); box.setPeer(new Peer()); sink(box.getPayload()); + } + + public static void directNodeSetter() { + Box box = new Box(); box.setPayload(source()); box.setNode(new Node()); sink(box.getPayload()); + } + + public static void nullPeerSetter() { + Box box = new Box(); box.setPayload(source()); box.setPeer(null); sink(box.getPayload()); + } + + public static void identityMetadataSetter() { + Box box = new Box(); box.setPayload(source()); box.setLabel(identity("safe")); sink(box.getPayload()); + } + + public static void aliasedReceiverSetter() { + Box box = new Box(); box.setPayload(source()); alias(box).setLabel("safe"); sink(box.getPayload()); + } + + public static void castReceiverSetter() { + Box box = new Box(); box.setPayload(source()); ((Box) box).setCategory("safe"); sink(box.getPayload()); + } + + public static void twoDifferentReferenceSetters() { + Box box = new Box(); box.setPayload(source()); box.setLabel("safe"); box.setPeer(new Peer()); sink(box.getPayload()); + } + + public static void sameReferenceSetterTwice() { + Box box = new Box(); box.setPayload(source()); box.setLabel("first"); box.setLabel("second"); sink(box.getPayload()); + } + + public static void holderConstructorAfterTaint() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(box); sink(holder.box.getPayload()); + } + + public static void namedHolderConstructorAfterTaint() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(box, "safe"); sink(holder.box.getPayload()); + } + + public static void holderFactoryAfterTaint() { + Box box = new Box(); box.setPayload(source()); Holder holder = makeHolder(box); sink(holder.box.getPayload()); + } + + public static void namedHolderFactoryAfterTaint() { + Box box = new Box(); box.setPayload(source()); Holder holder = makeHolder(box, "safe"); sink(holder.box.getPayload()); + } + + public static void envelopeConstructorAfterTaint() { + Box box = new Box(); box.setPayload(source()); Envelope envelope = new Envelope(new Holder(box)); sink(envelope.holder.box.getPayload()); + } + + public static void pairConstructorFirstArgument() { + Box box = new Box(); box.setPayload(source()); Pair pair = new Pair(box, new Box()); sink(pair.first.getPayload()); + } + + public static void pairConstructorSecondArgument() { + Box box = new Box(); box.setPayload(source()); Pair pair = new Pair(new Box(), box); sink(pair.second.getPayload()); + } + + public static void assignBoxThroughHolderSetter() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(); holder.setBox(box); sink(holder.box.getPayload()); + } + + public static void holderConstructorAliasArgument() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(alias(box)); sink(holder.box.getPayload()); + } + + public static void holderConstructorLocalAlias() { + Box box = new Box(); box.setPayload(source()); Box other = box; Holder holder = new Holder(other); sink(holder.box.getPayload()); + } + + public static void holderConstructorCastArgument() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder((Box) box); sink(holder.box.getPayload()); + } + + public static void holderConstructorNullMetadata() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(box, null); sink(holder.box.getPayload()); + } + + public static void holderConstructorIdentityMetadata() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(box, identity("safe")); sink(holder.box.getPayload()); + } + + public static void tripleConstructorFirstArgument() { + Box box = new Box(); box.setPayload(source()); Triple value = new Triple(box, new Box(), new Box()); sink(value.first.getPayload()); + } + + public static void tripleConstructorMiddleArgument() { + Box box = new Box(); box.setPayload(source()); Triple value = new Triple(new Box(), box, new Box()); sink(value.second.getPayload()); + } + + public static void tripleConstructorLastArgument() { + Box box = new Box(); box.setPayload(source()); Triple value = new Triple(new Box(), new Box(), box); sink(value.third.getPayload()); + } + + public static void tripleFactoryFirstArgument() { + Box box = new Box(); box.setPayload(source()); Triple value = makeTripleFirst(box); sink(value.first.getPayload()); + } + + public static void pairFactoryFirstArgument() { + Box box = new Box(); box.setPayload(source()); Pair value = makePairFirst(box); sink(value.first.getPayload()); + } + + public static void pairFactorySecondArgument() { + Box box = new Box(); box.setPayload(source()); Pair value = makePairSecond(box); sink(value.second.getPayload()); + } + + public static void holderFactoryAliasedArgument() { + Box box = new Box(); box.setPayload(source()); Holder holder = makeAliasedHolder(box); sink(holder.box.getPayload()); + } + + public static void holderFactoryCastArgument() { + Box box = new Box(); box.setPayload(source()); Holder holder = makeHolder((Box) box); sink(holder.box.getPayload()); + } + + public static void nestedHolderFactory() { + Box box = new Box(); box.setPayload(source()); Envelope value = new Envelope(makeHolder(box)); sink(value.holder.box.getPayload()); + } + + public static void doubleEnvelopeConstructor() { + Box box = new Box(); box.setPayload(source()); DoubleEnvelope value = new DoubleEnvelope(new Envelope(new Holder(box))); sink(value.envelope.holder.box.getPayload()); + } + + public static void envelopeWithMetadataConstructor() { + Box box = new Box(); box.setPayload(source()); NamedEnvelope value = new NamedEnvelope(new Holder(box), "safe"); sink(value.holder.box.getPayload()); + } + + public static void holderSetterViaHelper() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(); installBox(holder, box); sink(holder.box.getPayload()); + } + + public static void holderSetterWithMetadataViaHelper() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(); installBox(holder, box, "safe"); sink(holder.box.getPayload()); + } + + public static void holderSetterAliasArgument() { + Box box = new Box(); box.setPayload(source()); Box other = box; Holder holder = new Holder(); holder.setBox(other); sink(holder.box.getPayload()); + } + + public static void holderSetterCastArgument() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(); holder.setBox((Box) box); sink(holder.box.getPayload()); + } + + public static void holderOverwriteSafeThenTainted() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(new Box()); holder.setBox(box); sink(holder.box.getPayload()); + } + + public static void holderOverwriteTaintedTwice() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(); holder.setBox(box); holder.setBox(box); sink(holder.box.getPayload()); + } + + public static void alternateHolderPrimaryField() { + Box box = new Box(); box.setPayload(source()); AlternateHolder holder = new AlternateHolder(); holder.setPrimary(box); sink(holder.primary.getPayload()); + } + + public static void alternateHolderSecondaryField() { + Box box = new Box(); box.setPayload(source()); AlternateHolder holder = new AlternateHolder(); holder.setSecondary(box); sink(holder.secondary.getPayload()); + } + + public static void alternateConstructorPrimaryField() { + Box box = new Box(); box.setPayload(source()); AlternateHolder holder = new AlternateHolder(box, new Box()); sink(holder.primary.getPayload()); + } + + public static void alternateConstructorSecondaryField() { + Box box = new Box(); box.setPayload(source()); AlternateHolder holder = new AlternateHolder(new Box(), box); sink(holder.secondary.getPayload()); + } + + public static void alternateOverwritePrimaryField() { + Box box = new Box(); box.setPayload(source()); AlternateHolder holder = new AlternateHolder(new Box(), new Box()); holder.setPrimary(box); sink(holder.primary.getPayload()); + } + + public static void alternateOverwriteSecondaryField() { + Box box = new Box(); box.setPayload(source()); AlternateHolder holder = new AlternateHolder(new Box(), new Box()); holder.setSecondary(box); sink(holder.secondary.getPayload()); + } + + public static void namedHolderSetBoxAndName() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(); holder.setBoxAndName(box, "safe"); sink(holder.box.getPayload()); + } + + public static void namedHolderSetBoxAndNullName() { + Box box = new Box(); box.setPayload(source()); Holder holder = new Holder(); holder.setBoxAndName(box, null); sink(holder.box.getPayload()); + } + + public static void pairThenEnvelopeFirstField() { + Box box = new Box(); box.setPayload(source()); Pair pair = new Pair(box, new Box()); PairEnvelope value = new PairEnvelope(pair); sink(value.pair.first.getPayload()); + } + + public static void pairThenEnvelopeSecondField() { + Box box = new Box(); box.setPayload(source()); Pair pair = new Pair(new Box(), box); PairEnvelope value = new PairEnvelope(pair); sink(value.pair.second.getPayload()); + } + + public static void quadConstructorFirstArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(box, new Box(), new Box(), new Box()); sink(value.first.getPayload()); + } + + public static void quadConstructorSecondArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(new Box(), box, new Box(), new Box()); sink(value.second.getPayload()); + } + + public static void quadConstructorThirdArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(new Box(), new Box(), box, new Box()); sink(value.third.getPayload()); + } + + public static void quadConstructorFourthArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(new Box(), new Box(), new Box(), box); sink(value.fourth.getPayload()); + } + + public static void quadFactoryFirstArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = makeQuadFirst(box); sink(value.first.getPayload()); + } + + public static void quadFactorySecondArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = makeQuadSecond(box); sink(value.second.getPayload()); + } + + public static void quadFactoryThirdArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = makeQuadThird(box); sink(value.third.getPayload()); + } + + public static void quadFactoryFourthArgument() { + Box box = new Box(); box.setPayload(source()); Quad value = makeQuadFourth(box); sink(value.fourth.getPayload()); + } + + public static void keyedConstructorLeftField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(box, new Box(), new Box()); sink(value.left.getPayload()); + } + + public static void keyedConstructorCenterField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(new Box(), box, new Box()); sink(value.center.getPayload()); + } + + public static void keyedConstructorRightField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(new Box(), new Box(), box); sink(value.right.getPayload()); + } + + public static void keyedSetterLeftField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(); value.setLeft(box); sink(value.left.getPayload()); + } + + public static void keyedSetterCenterField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(); value.setCenter(box); sink(value.center.getPayload()); + } + + public static void keyedSetterRightField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(); value.setRight(box); sink(value.right.getPayload()); + } + + public static void alternatePrimaryViaHelper() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); installPrimary(value, box); sink(value.primary.getPayload()); + } + + public static void alternateSecondaryViaHelper() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); installSecondary(value, box); sink(value.secondary.getPayload()); + } + + public static void alternatePrimaryViaNestedHelper() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); nestedInstallPrimary(value, box); sink(value.primary.getPayload()); + } + + public static void alternatePrimaryViaFactory() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = makeAlternatePrimary(box); sink(value.primary.getPayload()); + } + + public static void alternatePrimaryNullThenTainted() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); value.setPrimary(null); value.setPrimary(box); sink(value.primary.getPayload()); + } + + public static void alternateSecondaryNullThenTainted() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); value.setSecondary(null); value.setSecondary(box); sink(value.secondary.getPayload()); + } + + public static void alternatePrimarySafeThenTaintedViaHelper() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(new Box(), new Box()); installPrimary(value, box); sink(value.primary.getPayload()); + } + + public static void holderNullThenTainted() { + Box box = new Box(); box.setPayload(source()); Holder value = new Holder(); value.setBox(null); value.setBox(box); sink(value.box.getPayload()); + } + + public static void holderTaintedNullThenTainted() { + Box box = new Box(); box.setPayload(source()); Holder value = new Holder(); value.setBox(box); value.setBox(null); value.setBox(box); sink(value.box.getPayload()); + } + + public static void quadEnvelopeFirstField() { + Box box = new Box(); box.setPayload(source()); QuadEnvelope value = new QuadEnvelope(new Quad(box, new Box(), new Box(), new Box())); sink(value.quad.first.getPayload()); + } + + public static void quadEnvelopeFourthField() { + Box box = new Box(); box.setPayload(source()); QuadEnvelope value = new QuadEnvelope(new Quad(new Box(), new Box(), new Box(), box)); sink(value.quad.fourth.getPayload()); + } + + public static void alternateEnvelopePrimaryField() { + Box box = new Box(); box.setPayload(source()); AlternateEnvelope value = new AlternateEnvelope(new AlternateHolder(box, new Box())); sink(value.holder.primary.getPayload()); + } + + public static void alternateEnvelopeSecondaryField() { + Box box = new Box(); box.setPayload(source()); AlternateEnvelope value = new AlternateEnvelope(new AlternateHolder(new Box(), box)); sink(value.holder.secondary.getPayload()); + } + + public static void alternateSetBothFirstArgument() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); value.setBoth(box, new Box()); sink(value.primary.getPayload()); + } + + public static void alternateSetBothSecondArgument() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); value.setBoth(new Box(), box); sink(value.secondary.getPayload()); + } + + public static void keyedSetAllCenterArgument() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(); value.setAll(new Box(), box, new Box()); sink(value.center.getPayload()); + } + + public static void quadConstructorFirstWithNullPeers() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(box, null, null, null); sink(value.first.getPayload()); + } + + public static void quadConstructorSecondWithNullPeers() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(null, box, null, null); sink(value.second.getPayload()); + } + + public static void quadConstructorThirdWithNullPeers() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(null, null, box, null); sink(value.third.getPayload()); + } + + public static void quadConstructorFourthWithNullPeers() { + Box box = new Box(); box.setPayload(source()); Quad value = new Quad(null, null, null, box); sink(value.fourth.getPayload()); + } + + public static void keyedConstructorLeftWithNullPeers() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(box, null, null); sink(value.left.getPayload()); + } + + public static void keyedConstructorCenterWithNullPeers() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(null, box, null); sink(value.center.getPayload()); + } + + public static void keyedConstructorRightWithNullPeers() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(null, null, box); sink(value.right.getPayload()); + } + + public static void keyedEnvelopeLeftField() { + Box box = new Box(); box.setPayload(source()); KeyedEnvelope value = new KeyedEnvelope(new KeyedHolder(box, new Box(), new Box())); sink(value.holder.left.getPayload()); + } + + public static void keyedEnvelopeCenterField() { + Box box = new Box(); box.setPayload(source()); KeyedEnvelope value = new KeyedEnvelope(new KeyedHolder(new Box(), box, new Box())); sink(value.holder.center.getPayload()); + } + + public static void keyedEnvelopeRightField() { + Box box = new Box(); box.setPayload(source()); KeyedEnvelope value = new KeyedEnvelope(new KeyedHolder(new Box(), new Box(), box)); sink(value.holder.right.getPayload()); + } + + public static void doubleAlternateEnvelopePrimaryField() { + Box box = new Box(); box.setPayload(source()); DoubleAlternateEnvelope value = new DoubleAlternateEnvelope(new AlternateEnvelope(new AlternateHolder(box, new Box()))); sink(value.envelope.holder.primary.getPayload()); + } + + public static void doubleAlternateEnvelopeSecondaryField() { + Box box = new Box(); box.setPayload(source()); DoubleAlternateEnvelope value = new DoubleAlternateEnvelope(new AlternateEnvelope(new AlternateHolder(new Box(), box))); sink(value.envelope.holder.secondary.getPayload()); + } + + public static void keyedOverwriteLeftSafeThenTainted() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(new Box(), new Box(), new Box()); value.setLeft(box); sink(value.left.getPayload()); + } + + public static void keyedOverwriteCenterSafeThenTainted() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(new Box(), new Box(), new Box()); value.setCenter(box); sink(value.center.getPayload()); + } + + public static void keyedOverwriteRightSafeThenTainted() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = new KeyedHolder(new Box(), new Box(), new Box()); value.setRight(box); sink(value.right.getPayload()); + } + + public static void alternateSetBothFirstWithNullPeer() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); value.setBoth(box, null); sink(value.primary.getPayload()); + } + + public static void alternateSetBothSecondWithNullPeer() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(); value.setBoth(null, box); sink(value.secondary.getPayload()); + } + + public static void keyedFactoryLeftField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = makeKeyedLeft(box); sink(value.left.getPayload()); + } + + public static void keyedFactoryCenterField() { + Box box = new Box(); box.setPayload(source()); KeyedHolder value = makeKeyedCenter(box); sink(value.center.getPayload()); + } + + public static void alternateConstructorPrimaryWithNullPeer() { + Box box = new Box(); box.setPayload(source()); AlternateHolder value = new AlternateHolder(box, null); sink(value.primary.getPayload()); + } + + public static void helperStringSetter() { + Box box = new Box(); box.setPayload(source()); setLabel(box, "safe"); sink(box.getPayload()); + } + + public static void helperPeerSetter() { + Box box = new Box(); box.setPayload(source()); setPeer(box, new Peer()); sink(box.getPayload()); + } + + public static void nestedHelperStringSetter() { + Box box = new Box(); box.setPayload(source()); nestedSetLabel(box, "safe"); sink(box.getPayload()); + } + + public static void helperReturnsMutatedReceiver() { + Box box = new Box(); box.setPayload(source()); box = touchAndReturn(box, "safe"); sink(box.getPayload()); + } + + public static void helperOnAliasedReceiver() { + Box box = new Box(); box.setPayload(source()); Box other = box; setLabel(other, "safe"); sink(box.getPayload()); + } + + public static void directThenHelperSetter() { + Box box = new Box(); box.setPayload(source()); box.setCategory("safe"); setLabel(box, "safe"); sink(box.getPayload()); + } + + public static void helperThenDirectSetter() { + Box box = new Box(); box.setPayload(source()); setLabel(box, "safe"); box.setCategory("safe"); sink(box.getPayload()); + } + + public static void twoArgumentInstanceSetter() { + Box box = new Box(); box.setPayload(source()); box.setMetadata("left", "right"); sink(box.getPayload()); + } + + public static void twoArgumentHelperSetter() { + Box box = new Box(); box.setPayload(source()); setBoth(box, "left", "right"); sink(box.getPayload()); + } + + public static void twoReferenceInstanceSetter() { + Box box = new Box(); box.setPayload(source()); box.setReferences(new Peer(), new Node()); sink(box.getPayload()); + } + + public static void twoReferenceHelperSetter() { + Box box = new Box(); box.setPayload(source()); setReferences(box, new Peer(), new Node()); sink(box.getPayload()); + } + + public static void multiArgumentSetterWithAliases() { + Box box = new Box(); box.setPayload(source()); String left = "left"; String right = left; box.setMetadata(left, right); sink(box.getPayload()); + } + + public static void multiArgumentSetterWithNull() { + Box box = new Box(); box.setPayload(source()); box.setMetadata(null, "right"); sink(box.getPayload()); + } + + public static void overwritePeerTwice() { + Box box = new Box(); box.setPayload(source()); box.setPeer(new Peer()); box.setPeer(new Peer()); sink(box.getPayload()); + } + + public static void overwriteNodeWithNull() { + Box box = new Box(); box.setPayload(source()); box.setNode(new Node()); box.setNode(null); sink(box.getPayload()); + } + + public static void overwriteLabelNullThenValue() { + Box box = new Box(); box.setPayload(source()); box.setLabel(null); box.setLabel("safe"); sink(box.getPayload()); + } + + public static void overwriteCategoryViaIdentity() { + Box box = new Box(); box.setPayload(source()); box.setCategory("first"); box.setCategory(identity("second")); sink(box.getPayload()); + } + + public static void prebuiltContainerFieldSetter() { + Container container = new Container(new Box()); container.box.setPayload(source()); container.box.setLabel("safe"); sink(container.box.getPayload()); + } + + public static void prebuiltDoubleContainerFieldSetter() { + DoubleContainer root = new DoubleContainer(new Container(new Box())); root.container.box.setPayload(source()); root.container.box.setCategory("safe"); sink(root.container.box.getPayload()); + } + + public static void fieldChainAliasSetter() { + Container container = new Container(new Box()); container.box.setPayload(source()); Box local = container.box; local.setPeer(new Peer()); sink(container.box.getPayload()); + } + + public static void fieldChainHelperSetter() { + Container container = new Container(new Box()); container.box.setPayload(source()); setLabel(container.box, "safe"); sink(container.box.getPayload()); + } + + public static void siblingFieldSetterAfterNestedTaint() { + Container container = new Container(new Box()); container.box.setPayload(source()); container.setName("safe"); sink(container.box.getPayload()); + } + + private static class Box { + private String payload; + private String label; + private String category; + private Peer peer; + private Node node; + void setPayload(String value) { payload = value; } + String getPayload() { return payload; } + void setLabel(String value) { label = value; } + void setCategory(String value) { category = value; } + void setPeer(Peer value) { peer = value; } + void setNode(Node value) { node = value; } + void setMetadata(String first, String second) { label = first; category = second; } + void setReferences(Peer first, Node second) { peer = first; node = second; } + } + + private static final class Peer { } + private static final class Node { } + + private static final class Holder { + private Box box; + private String name; + Holder() { } + Holder(Box box) { this.box = box; } + Holder(Box box, String name) { this.box = box; this.name = name; } + void setBox(Box value) { box = value; } + void setBoxAndName(Box value, String name) { box = value; this.name = name; } + } + + private static final class Envelope { + private final Holder holder; + Envelope(Holder holder) { this.holder = holder; } + } + + private static final class Pair { + private final Box first; + private final Box second; + Pair(Box first, Box second) { this.first = first; this.second = second; } + } + + private static final class Triple { + private final Box first; + private final Box second; + private final Box third; + Triple(Box first, Box second, Box third) { this.first = first; this.second = second; this.third = third; } + } + + private static final class Quad { + private final Box first; + private final Box second; + private final Box third; + private final Box fourth; + Quad(Box first, Box second, Box third, Box fourth) { this.first = first; this.second = second; this.third = third; this.fourth = fourth; } + } + + private static final class AlternateHolder { + private Box primary; + private Box secondary; + AlternateHolder() { } + AlternateHolder(Box primary, Box secondary) { this.primary = primary; this.secondary = secondary; } + void setPrimary(Box value) { primary = value; } + void setSecondary(Box value) { secondary = value; } + void setBoth(Box first, Box second) { primary = first; secondary = second; } + } + + private static final class KeyedHolder { + private Box left; + private Box center; + private Box right; + KeyedHolder() { } + KeyedHolder(Box left, Box center, Box right) { this.left = left; this.center = center; this.right = right; } + void setLeft(Box value) { left = value; } + void setCenter(Box value) { center = value; } + void setRight(Box value) { right = value; } + void setAll(Box left, Box center, Box right) { this.left = left; this.center = center; this.right = right; } + } + + private static final class QuadEnvelope { + private final Quad quad; + QuadEnvelope(Quad quad) { this.quad = quad; } + } + + private static final class AlternateEnvelope { + private final AlternateHolder holder; + AlternateEnvelope(AlternateHolder holder) { this.holder = holder; } + } + + private static final class KeyedEnvelope { + private final KeyedHolder holder; + KeyedEnvelope(KeyedHolder holder) { this.holder = holder; } + } + + private static final class DoubleAlternateEnvelope { + private final AlternateEnvelope envelope; + DoubleAlternateEnvelope(AlternateEnvelope envelope) { this.envelope = envelope; } + } + + private static final class DoubleEnvelope { + private final Envelope envelope; + DoubleEnvelope(Envelope envelope) { this.envelope = envelope; } + } + + private static final class NamedEnvelope { + private final Holder holder; + private final String name; + NamedEnvelope(Holder holder, String name) { this.holder = holder; this.name = name; } + } + + private static final class PairEnvelope { + private final Pair pair; + PairEnvelope(Pair pair) { this.pair = pair; } + } + + private static final class Container { + private final Box box; + private String name; + Container(Box box) { this.box = box; } + void setName(String value) { name = value; } + } + + private static final class DoubleContainer { + private final Container container; + DoubleContainer(Container container) { this.container = container; } + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyReferenceTransferFuzzSample.java b/core/samples/src/main/java/test/samples/BaseOnlyReferenceTransferFuzzSample.java new file mode 100644 index 000000000..2a66edc4d --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyReferenceTransferFuzzSample.java @@ -0,0 +1,81 @@ +package test.samples; + +public class BaseOnlyReferenceTransferFuzzSample { + private static String source() { return "tainted"; } + private static void sink(String value) { } + private static String identity(String value) { return value; } + + private static PayloadBox makeBox(String value) { return new PayloadBox(value); } + private static PayloadBox makeBoxDelegated(String value) { return makeBox(value); } + private static Envelope makeEnvelope(PayloadBox box) { return new Envelope(box); } + private static Envelope makeEnvelopeFromValue(String value) { return new Envelope(new PayloadBox(value)); } + private static Outer makeOuter(String value) { return new Outer(new Envelope(new PayloadBox(value))); } + private static void install(PayloadBox box, String value) { box.setPayload(value); } + private static void installDelegated(PayloadBox box, String value) { install(box, value); } + private static void installEnvelope(Envelope envelope, PayloadBox box) { envelope.setBox(box); } + + public static void directPayloadConstructor() { PayloadBox box = new PayloadBox(source()); sink(box.payload); } + public static void identityPayloadConstructor() { PayloadBox box = new PayloadBox(identity(source())); sink(box.payload); } + public static void payloadConstructorIntoLocal() { String value = source(); PayloadBox box = new PayloadBox(value); sink(box.payload); } + public static void nestedEnvelopeConstructors() { Envelope envelope = new Envelope(new PayloadBox(source())); sink(envelope.box.payload); } + public static void tripleNestedConstructors() { Outer outer = new Outer(new Envelope(new PayloadBox(source()))); sink(outer.envelope.box.payload); } + public static void constructorAfterValueAlias() { String value = source(); String alias = value; PayloadBox box = new PayloadBox(alias); sink(box.payload); } + public static void constructorAfterTwoValueAliases() { String value = source(); String first = value; String second = first; PayloadBox box = new PayloadBox(second); sink(box.payload); } + public static void constructorThenWrapperAlias() { PayloadBox box = new PayloadBox(source()); PayloadBox alias = box; sink(alias.payload); } + + public static void directBoxFactory() { PayloadBox box = makeBox(source()); sink(box.payload); } + public static void delegatedBoxFactory() { PayloadBox box = makeBoxDelegated(source()); sink(box.payload); } + public static void envelopeFactory() { Envelope envelope = makeEnvelope(new PayloadBox(source())); sink(envelope.box.payload); } + public static void envelopeFactoryFromValue() { Envelope envelope = makeEnvelopeFromValue(source()); sink(envelope.box.payload); } + public static void outerFactoryFromValue() { Outer outer = makeOuter(source()); sink(outer.envelope.box.payload); } + public static void factoryAfterValueAlias() { String value = source(); String alias = value; PayloadBox box = makeBox(alias); sink(box.payload); } + + public static void directPayloadSetter() { PayloadBox box = new PayloadBox(); box.setPayload(source()); sink(box.payload); } + public static void setterAfterIdentity() { PayloadBox box = new PayloadBox(); box.setPayload(identity(source())); sink(box.payload); } + public static void setterOnAliasedWrapper() { PayloadBox box = new PayloadBox(); PayloadBox alias = box; alias.setPayload(source()); sink(box.payload); } + public static void helperPayloadSetter() { PayloadBox box = new PayloadBox(); install(box, source()); sink(box.payload); } + public static void delegatedHelperPayloadSetter() { PayloadBox box = new PayloadBox(); installDelegated(box, source()); sink(box.payload); } + public static void helperSetterOnAlias() { PayloadBox box = new PayloadBox(); PayloadBox alias = box; install(alias, source()); sink(box.payload); } + public static void envelopeSetterAfterPayloadConstructor() { Envelope envelope = new Envelope(); installEnvelope(envelope, new PayloadBox(source())); sink(envelope.box.payload); } + public static void envelopeSetterAfterPayloadSetter() { PayloadBox box = new PayloadBox(); box.setPayload(source()); Envelope envelope = new Envelope(); envelope.setBox(box); sink(envelope.box.payload); } + + public static void fluentPayloadSetter() { PayloadBox box = new PayloadBox().withPayload(source()); sink(box.payload); } + public static void fluentPayloadAfterIdentity() { PayloadBox box = new PayloadBox().withPayload(identity(source())); sink(box.payload); } + public static void fluentNestedEnvelope() { Envelope envelope = new Envelope().withBox(new PayloadBox().withPayload(source())); sink(envelope.box.payload); } + + public static void pairConstructorFirstPayload() { PayloadPair pair = new PayloadPair(source(), "clean"); sink(pair.first); } + public static void pairConstructorSecondPayload() { PayloadPair pair = new PayloadPair("clean", source()); sink(pair.second); } + public static void pairSetterFirstPayload() { PayloadPair pair = new PayloadPair(); pair.setBoth(source(), "clean"); sink(pair.first); } + public static void pairSetterSecondPayload() { PayloadPair pair = new PayloadPair(); pair.setBoth("clean", source()); sink(pair.second); } + public static void referenceArrayWrapper() { ArrayEnvelope envelope = new ArrayEnvelope(new String[]{source()}); sink(envelope.values[0]); } + + private static class PayloadBox { + private String payload; + PayloadBox() { } + PayloadBox(String payload) { this.payload = payload; } + void setPayload(String value) { payload = value; } + PayloadBox withPayload(String value) { payload = value; return this; } + } + private static final class Envelope { + private PayloadBox box; + Envelope() { } + Envelope(PayloadBox box) { this.box = box; } + void setBox(PayloadBox box) { this.box = box; } + Envelope withBox(PayloadBox box) { this.box = box; return this; } + } + private static final class Outer { + private final Envelope envelope; + Outer(Envelope envelope) { this.envelope = envelope; } + } + private static final class PayloadPair { + private String first; + private String second; + PayloadPair() { } + PayloadPair(String first, String second) { this.first = first; this.second = second; } + void setBoth(String first, String second) { this.first = first; this.second = second; } + } + private static final class ArrayEnvelope { + private final String[] values; + ArrayEnvelope(String[] values) { this.values = values; } + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlySummaryFieldExplosionSample.java b/core/samples/src/main/java/test/samples/BaseOnlySummaryFieldExplosionSample.java new file mode 100644 index 000000000..d577b681b --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlySummaryFieldExplosionSample.java @@ -0,0 +1,161 @@ +package test.samples; + +public class BaseOnlySummaryFieldExplosionSample { + private static String source() { + return "tainted"; + } + + private static void sink(String value) { + } + + public static void fieldEnumerationExplosion(int readSelector, int writeSelector) { + Fields input = new Fields(); + String tainted = source(); + input.f00 = tainted; + input.f01 = tainted; + input.f02 = tainted; + input.f03 = tainted; + input.f04 = tainted; + input.f05 = tainted; + input.f06 = tainted; + input.f07 = tainted; + input.f08 = tainted; + input.f09 = tainted; + input.f10 = tainted; + input.f11 = tainted; + input.f12 = tainted; + input.f13 = tainted; + input.f14 = tainted; + input.f15 = tainted; + input.f16 = tainted; + input.f17 = tainted; + input.f18 = tainted; + input.f19 = tainted; + + Fields result = permuteField(input, readSelector, writeSelector); + sink(result.f00); + } + + public static void exactFinalConvergence(int readSelector) { + Fields input = new Fields(); + String tainted = source(); + input.f00 = tainted; + input.f01 = tainted; + + sink(input.f00); + convergeFieldPremises(input, readSelector); + } + + public static void irrelevantCallConclusionSharing(int readSelector) { + Fields input = new Fields(); + String tainted = source(); + input.f00 = tainted; + input.f01 = tainted; + + String selected = convergeFieldPremisesAcrossIrrelevantCall(input, readSelector); + sink(selected); + } + + private static String convergeFieldPremisesAcrossIrrelevantCall(Fields input, int readSelector) { + String selected; + switch (readSelector) { + case 0: selected = input.f00; break; + default: selected = input.f01; + } + passthrough(selected); + irrelevantCall(); + return selected; + } + + private static void irrelevantCall() { + } + + private static Fields permuteField( + Fields input, + int readSelector, + int writeSelector) { + String selected; + switch (readSelector) { + case 0: selected = input.f00; break; + case 1: selected = input.f01; break; + case 2: selected = input.f02; break; + case 3: selected = input.f03; break; + case 4: selected = input.f04; break; + case 5: selected = input.f05; break; + case 6: selected = input.f06; break; + case 7: selected = input.f07; break; + case 8: selected = input.f08; break; + case 9: selected = input.f09; break; + case 10: selected = input.f10; break; + case 11: selected = input.f11; break; + case 12: selected = input.f12; break; + case 13: selected = input.f13; break; + case 14: selected = input.f14; break; + case 15: selected = input.f15; break; + case 16: selected = input.f16; break; + case 17: selected = input.f17; break; + case 18: selected = input.f18; break; + default: selected = input.f19; + } + + switch (writeSelector) { + case 0: input.f00 = selected; break; + case 1: input.f01 = selected; break; + case 2: input.f02 = selected; break; + case 3: input.f03 = selected; break; + case 4: input.f04 = selected; break; + case 5: input.f05 = selected; break; + case 6: input.f06 = selected; break; + case 7: input.f07 = selected; break; + case 8: input.f08 = selected; break; + case 9: input.f09 = selected; break; + case 10: input.f10 = selected; break; + case 11: input.f11 = selected; break; + case 12: input.f12 = selected; break; + case 13: input.f13 = selected; break; + case 14: input.f14 = selected; break; + case 15: input.f15 = selected; break; + case 16: input.f16 = selected; break; + case 17: input.f17 = selected; break; + case 18: input.f18 = selected; break; + default: input.f19 = selected; + } + return input; + } + + private static void convergeFieldPremises(Fields input, int readSelector) { + String selected; + switch (readSelector) { + case 0: selected = input.f00; break; + default: selected = input.f01; + } + passthrough(selected); + } + + private static String passthrough(String value) { + return value; + } + + private static class Fields { + String f00; + String f01; + String f02; + String f03; + String f04; + String f05; + String f06; + String f07; + String f08; + String f09; + String f10; + String f11; + String f12; + String f13; + String f14; + String f15; + String f16; + String f17; + String f18; + String f19; + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyTraceProjectionFuzzSample.java b/core/samples/src/main/java/test/samples/BaseOnlyTraceProjectionFuzzSample.java new file mode 100644 index 000000000..67d840afd --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyTraceProjectionFuzzSample.java @@ -0,0 +1,63 @@ +package test.samples; + +public class BaseOnlyTraceProjectionFuzzSample { + private static Outer source() { return null; } + private static void sink(Token value) { } + + private static Envelope projectEnvelope(Outer value) { + return value.envelope; + } + + public static void projectOneLevel() { + Outer value = source(); + Envelope result = projectEnvelope(value); + sink(result.box.value); + } + + private static Token projectToken(Outer value) { + return projectEnvelope(value).box.value; + } + + public static void projectThreeLevels() { + Outer value = source(); + sink(projectToken(value)); + } + + private static Outer relayOuter(Outer value) { return value; } + private static Envelope relayEnvelope(Envelope value) { return value; } + + public static void relayThenProject() { + Outer value = relayOuter(source()); + Envelope result = relayEnvelope(projectEnvelope(value)); + sink(result.box.value); + } + + private static void touchOuter(Outer value) { value.other = new Token(); } + private static void touchEnvelope(Envelope value) { value.other = new Token(); } + private static void touchBox(Box value) { value.other = new Token(); } + + public static void mutateThenProject() { + Outer value = source(); + touchOuter(value); + touchEnvelope(value.envelope); + touchBox(value.envelope.box); + sink(projectToken(value)); + } + + private static final class Token { } + + private static final class Box { + Token value; + Token other; + } + + private static final class Envelope { + Box box; + Token other; + } + + private static final class Outer { + Envelope envelope; + Token other; + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyTraceRelayFuzzSample.java b/core/samples/src/main/java/test/samples/BaseOnlyTraceRelayFuzzSample.java new file mode 100644 index 000000000..9125705f0 --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyTraceRelayFuzzSample.java @@ -0,0 +1,106 @@ +package test.samples; + +public class BaseOnlyTraceRelayFuzzSample { + private static Token source() { return new Token(); } + private static void sink(Token value) { } + + private static Envelope identity(Envelope value) { return value; } + + private static Envelope identityFactory(Token value) { + Envelope envelope = new Envelope(new Box(value)); + return identity(envelope); + } + + public static void returnThroughIdentity() { + Envelope result = identityFactory(source()); + sink(result.box.value); + } + + private static Envelope doubleIdentityFactory(Token value) { + Envelope envelope = new Envelope(new Box(value)); + return identity(identity(envelope)); + } + + public static void returnThroughDoubleIdentity() { + Envelope result = doubleIdentityFactory(source()); + sink(result.box.value); + } + + private static Envelope instanceIdentityFactory(Token value) { + Envelope envelope = new Envelope(new Box(value)); + return envelope.self(); + } + + public static void returnThroughInstanceIdentity() { + Envelope result = instanceIdentityFactory(source()); + sink(result.box.value); + } + + private static Envelope interfaceIdentityFactory(Token value) { + Envelope envelope = new Envelope(new Box(value)); + EnvelopeRelay relay = new EnvelopeRelayImpl(); + return relay.relay(envelope); + } + + public static void returnThroughInterfaceIdentity() { + Envelope result = interfaceIdentityFactory(source()); + sink(result.box.value); + } + + private static Envelope branchIdentityFactory(Token value) { + Envelope envelope = new Envelope(new Box(value)); + return choose(envelope, new Envelope(new Box())); + } + + private static Envelope choose(Envelope first, Envelope second) { + return first != null ? first : second; + } + + public static void returnThroughBranchIdentity() { + Envelope result = branchIdentityFactory(source()); + sink(result.box.value); + } + + private static Outer outerIdentity(Outer value) { return value; } + + private static Outer outerIdentityFactory(Token value) { + Outer outer = new Outer(new Envelope(new Box(value))); + return outerIdentity(outer); + } + + public static void returnOuterThroughIdentity() { + Outer result = outerIdentityFactory(source()); + sink(result.envelope.box.value); + } + + private static final class Token { } + + private static final class Box { + Token value; + + Box() { } + Box(Token value) { this.value = value; } + } + + private static final class Envelope { + Box box; + + Envelope(Box box) { this.box = box; } + Envelope self() { return this; } + } + + private static final class Outer { + Envelope envelope; + + Outer(Envelope envelope) { this.envelope = envelope; } + } + + private interface EnvelopeRelay { + Envelope relay(Envelope value); + } + + private static final class EnvelopeRelayImpl implements EnvelopeRelay { + @Override + public Envelope relay(Envelope value) { return value; } + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyTraceResolutionFuzzSample.java b/core/samples/src/main/java/test/samples/BaseOnlyTraceResolutionFuzzSample.java new file mode 100644 index 000000000..49ea74e76 --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyTraceResolutionFuzzSample.java @@ -0,0 +1,25 @@ +package test.samples; + +public class BaseOnlyTraceResolutionFuzzSample { + private static String source() { return "tainted"; } + private static void sink(String value) { } + + private static Envelope envelope(String value) { return new Envelope(new Box(value)); } + + public static void nestedFactory() { + Envelope result = envelope(source()); + sink(result.box.value); + } + + private static final class Box { + String value; + + private Box(String value) { this.value = value; } + } + + private static final class Envelope { + final Box box; + + private Envelope(Box box) { this.box = box; } + } +} diff --git a/core/samples/src/main/java/test/samples/BaseOnlyTraceShapeFuzzSample.java b/core/samples/src/main/java/test/samples/BaseOnlyTraceShapeFuzzSample.java new file mode 100644 index 000000000..c15c8dec4 --- /dev/null +++ b/core/samples/src/main/java/test/samples/BaseOnlyTraceShapeFuzzSample.java @@ -0,0 +1,67 @@ +package test.samples; + +public class BaseOnlyTraceShapeFuzzSample { + private static Token source() { return new Token(); } + private static void sink(Response value) { } + + private static Response response(Token value) { + Response response = new Response(); + response.body = value; + return response; + } + + private static Response probe(Response value) { return value; } + private static Response relay(Response value) { return value; } + private static Response project(Outer value) { return value.response; } + + private static Outer probedFactory(Token value) { + Outer outer = new Outer(); + outer.response = probe(response(value)); + return outer; + } + + private static Outer probedConstructorFactory(Token value) { + return new Outer(probe(response(value))); + } + + private static Outer doubleProbedFactory(Token value) { + Outer outer = new Outer(); + outer.response = probe(probe(response(value))); + return outer; + } + + private static Outer relayedProbeFactory(Token value) { + Outer outer = new Outer(); + outer.response = relay(probe(response(value))); + return outer; + } + + public static void projectedProbedFactory() { + sink(project(probedFactory(source()))); + } + + public static void projectedProbedConstructorFactory() { + sink(project(probedConstructorFactory(source()))); + } + + public static void projectedDoubleProbedFactory() { + sink(project(doubleProbedFactory(source()))); + } + + public static void projectedRelayedProbeFactory() { + sink(project(relayedProbeFactory(source()))); + } + + private static final class Token { } + + private static final class Response { + Token body; + } + + private static final class Outer { + Response response; + + Outer() { } + Outer(Response response) { this.response = response; } + } +} diff --git a/core/samples/src/main/java/test/samples/CollectionElementGetterSample.java b/core/samples/src/main/java/test/samples/CollectionElementGetterSample.java new file mode 100644 index 000000000..93c98ae41 --- /dev/null +++ b/core/samples/src/main/java/test/samples/CollectionElementGetterSample.java @@ -0,0 +1,231 @@ +package test.samples; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class CollectionElementGetterSample { + @GetMapping + public void searchViaList(ProductCriteria searchCriterias) { + facade(searchCriterias); + } + + @GetMapping + public void searchDirectField(ProductCriteria searchCriterias) { + sink(searchCriterias.getName()); + } + + @GetMapping + public void searchListOfStrings(ProductCriteria searchCriterias) { + StringBuilder qs = new StringBuilder(); + for (String code : searchCriterias.getCodes()) { + qs.append(code); + } + sink(qs.toString()); + } + + @GetMapping + public void searchShopizerShape(String lang, ProductCriteria searchCriterias, Integer count) { + if (searchCriterias.getSku() != null) { + searchCriterias.setCode(searchCriterias.getSku()); + } + facadeShopizer(searchCriterias); + } + + private void facadeShopizer(ProductCriteria criterias) { + serviceShopizer(criterias); + } + + private void serviceShopizer(ProductCriteria criteria) { + criteria.setPageSize(10); + repositoryShopizer(criteria); + } + + private void repositoryShopizer(ProductCriteria criteria) { + StringBuilder builderSelect = new StringBuilder(); + StringBuilder builderWhere = new StringBuilder(); + builderSelect.append("select p from Product p"); + if (criteria.getStatus() != null) { + builderWhere.append(" and p.status=:st"); + } + if (criteria.getAttributeCriteria() != null && !criteria.getAttributeCriteria().isEmpty()) { + int c = 0; + for (AttributeCriteria attributeCriteria : criteria.getAttributeCriteria()) { + if (c == 0) { + builderWhere.append(" and po.code =:").append(attributeCriteria.getAttributeCode()); + builderWhere.append(" and povd.description like :").append("val").append(c) + .append(attributeCriteria.getAttributeCode()); + } + c++; + } + if (criteria.getLanguage() != null) { + builderWhere.append(" and povd.language.code=:lang"); + } + } + if (criteria.getAvailable() != null) { + builderWhere.append(" and p.available=true"); + } + String hql = builderSelect.toString() + builderWhere.toString(); + sink(hql); + } + + private void facade(ProductCriteria criterias) { + service(criterias); + } + + private void service(ProductCriteria criteria) { + StringBuilder qs = new StringBuilder(); + for (AttributeCriteria attributeCriteria : criteria.getAttributeCriteria()) { + qs.append(attributeCriteria.getAttributeCode()); + } + sink(qs.toString()); + } + + public static void sink(String query) { + System.out.println(query); + } + + public static class AttributeCriteria { + private String attributeCode; + + private String attributeValue; + + public String getAttributeCode() { + return this.attributeCode; + } + + public String getAttributeValue() { + return this.attributeValue; + } + } + + public static class Criteria { + private int startIndex; + + private int maxCount; + + private String code; + + private String language; + + private String user; + + private String search; + + private int pageSize; + + public String getCode() { + return this.code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getLanguage() { + return this.language; + } + + public String getUser() { + return this.user; + } + + public String getSearch() { + return this.search; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public int getMaxCount() { + return this.maxCount; + } + + public int getStartIndex() { + return this.startIndex; + } + } + + public static class ProductCriteria extends Criteria { + private String name; + + private String productName; + + private List codes; + + private List attributeCriteria; + + private String origin; + + private Boolean available; + + private List categoryIds; + + private List availabilities; + + private List productIds; + + private List optionValueIds; + + private String sku; + + private String status; + + private Long manufacturerId; + + public String getName() { + return this.name; + } + + public String getProductName() { + return this.productName; + } + + public List getCodes() { + return this.codes; + } + + public List getAttributeCriteria() { + return this.attributeCriteria; + } + + public String getOrigin() { + return this.origin; + } + + public Boolean getAvailable() { + return this.available; + } + + public List getCategoryIds() { + return this.categoryIds; + } + + public List getAvailabilities() { + return this.availabilities; + } + + public List getProductIds() { + return this.productIds; + } + + public List getOptionValueIds() { + return this.optionValueIds; + } + + public String getSku() { + return this.sku; + } + + public String getStatus() { + return this.status; + } + + public Long getManufacturerId() { + return this.manufacturerId; + } + } +} diff --git a/core/samples/src/main/java/test/samples/ExplicitExceptionEdgesSample.java b/core/samples/src/main/java/test/samples/ExplicitExceptionEdgesSample.java new file mode 100644 index 000000000..2a6be4a1c --- /dev/null +++ b/core/samples/src/main/java/test/samples/ExplicitExceptionEdgesSample.java @@ -0,0 +1,29 @@ +package test.samples; + +public class ExplicitExceptionEdgesSample { + public static void caughtExplicitThrow(boolean fail) { + try { + implicitThrower(); + if (fail) { + throw new IllegalArgumentException("explicit"); + } + Runnable callback = () -> consume(new RuntimeException("lambda")); + callback.run(); + lastTryStatement(); + } catch (RuntimeException exception) { + consume(exception); + } + } + + private static void implicitThrower() { + throw new IllegalStateException("callee"); + } + + private static void lastTryStatement() { + // Keep a non-throw statement at the end of the protected region. + } + + private static void consume(RuntimeException exception) { + // Keep the catch handler in bytecode. + } +} diff --git a/core/samples/src/main/java/test/samples/GenericBridgeDispatchSample.java b/core/samples/src/main/java/test/samples/GenericBridgeDispatchSample.java new file mode 100644 index 000000000..f215d4ee6 --- /dev/null +++ b/core/samples/src/main/java/test/samples/GenericBridgeDispatchSample.java @@ -0,0 +1,52 @@ +package test.samples; + +public class GenericBridgeDispatchSample { + public static class Base { + } + + public static class Left extends Base { + Object payload; + } + + public static class Right extends Base { + Object payload; + } + + public abstract static class Validator { + public void validate(T value) { + validateImpl(value); + } + + protected abstract void validateImpl(T value); + } + + public static class LeftValidator extends Validator { + @Override + protected void validateImpl(Left value) { + } + } + + public static class RightValidator extends Validator { + @Override + protected void validateImpl(Right value) { + sink(source()); + } + } + + public static void incompatibleBridgeMustNotReturn(Validator validator) { + Left value = new Left(); + validator.validate(value); + } + + public static void compatibleBridgeMustReach(Validator validator) { + Right value = new Right(); + validator.validate(value); + } + + private static Object source() { + return new Object(); + } + + private static void sink(Object value) { + } +} diff --git a/core/samples/src/main/java/test/samples/KkFileViewSetterIdentityRegressionSample.java b/core/samples/src/main/java/test/samples/KkFileViewSetterIdentityRegressionSample.java new file mode 100644 index 000000000..7e7adfd5c --- /dev/null +++ b/core/samples/src/main/java/test/samples/KkFileViewSetterIdentityRegressionSample.java @@ -0,0 +1,37 @@ +package test.samples; + +public class KkFileViewSetterIdentityRegressionSample { + private static String source() { + return "untrusted"; + } + + private static void sink(String value) { + } + + public static void taintedLocalSurvivesUnrelatedSetters() { + String outFilePath = source(); + FileAttribute attribute = new FileAttribute(); + + attribute.setOutFilePath(outFilePath); + attribute.setType("finalized"); + + sink(attribute.getOutFilePath()); + } + + private static final class FileAttribute { + private String type; + private String outFilePath; + + void setType(String type) { + this.type = type; + } + + void setOutFilePath(String outFilePath) { + this.outFilePath = outFilePath; + } + + String getOutFilePath() { + return outFilePath; + } + } +} diff --git a/core/samples/src/main/java/test/samples/LibraryFragmentSample.java b/core/samples/src/main/java/test/samples/LibraryFragmentSample.java new file mode 100644 index 000000000..6063cae51 --- /dev/null +++ b/core/samples/src/main/java/test/samples/LibraryFragmentSample.java @@ -0,0 +1,36 @@ +package test.samples; + +import test.library.LibraryFragment; + +public final class LibraryFragmentSample { + + public interface LibraryBackedRepository extends LibraryFragment { + String findBySku(String sku); + } + + public static final class LibraryFragmentImpl implements LibraryFragment { + @Override + public String libraryQuery(String criteria) { + sink(criteria); + return criteria; + } + + @Override + public String libraryLookup(String url) { + return url; + } + } + + private final LibraryBackedRepository repository; + + public LibraryFragmentSample(LibraryBackedRepository repository) { + this.repository = repository; + } + + public void listProducts(String criteria) { + repository.libraryQuery(criteria); + } + + public static void sink(String query) { + } +} diff --git a/core/samples/src/main/java/test/samples/MethodOverridesCacheSample.java b/core/samples/src/main/java/test/samples/MethodOverridesCacheSample.java new file mode 100644 index 000000000..5e4ed9a55 --- /dev/null +++ b/core/samples/src/main/java/test/samples/MethodOverridesCacheSample.java @@ -0,0 +1,31 @@ +package test.samples; + +public class MethodOverridesCacheSample { + public static class Root { + public String value() { + return "clean"; + } + } + + public static class Left extends Root { + } + + public static class Right extends Root { + @Override + public String value() { + return source(); + } + } + + public void narrowCallMustNotReuseBroadOverrides(Root broad, Left left) { + broad.value(); + sink(left.value()); + } + + public static String source() { + return "tainted"; + } + + public static void sink(String value) { + } +} diff --git a/core/samples/src/main/java/test/samples/ObjectMethodDispatchSample.java b/core/samples/src/main/java/test/samples/ObjectMethodDispatchSample.java new file mode 100644 index 000000000..827236e11 --- /dev/null +++ b/core/samples/src/main/java/test/samples/ObjectMethodDispatchSample.java @@ -0,0 +1,28 @@ +package test.samples; + +public class ObjectMethodDispatchSample { + static class Value { + @Override + public int hashCode() { + sink(source()); + return 0; + } + } + + public void callThroughObjectMustBeIgnored() { + Object value = new Value(); + value.hashCode(); + } + + public void directOverrideCallRemainsAnalyzable() { + Value value = new Value(); + value.hashCode(); + } + + public static String source() { + return "tainted"; + } + + public static void sink(String value) { + } +} diff --git a/core/samples/src/main/java/test/samples/OverApproximateStartTraceSample.java b/core/samples/src/main/java/test/samples/OverApproximateStartTraceSample.java new file mode 100644 index 000000000..7241415c6 --- /dev/null +++ b/core/samples/src/main/java/test/samples/OverApproximateStartTraceSample.java @@ -0,0 +1,33 @@ +package test.samples; + +public class OverApproximateStartTraceSample { + public static String source() { + return "source"; + } + + public static void sink(String value) { + } + + private static String identity(String value) { + return value; + } + + private static String sourceOnEitherBranch(boolean firstBranch) { + String value; + if (firstBranch) { + value = source(); + } else { + value = source(); + } + return identity(value); + } + + public static void nonZeroSummary() { + String value = source(); + sink(identity(value)); + } + + public static void zeroSummary(boolean firstBranch) { + sink(sourceOnEitherBranch(firstBranch)); + } +} diff --git a/core/samples/src/main/java/test/samples/RepositoryFragmentSample.java b/core/samples/src/main/java/test/samples/RepositoryFragmentSample.java new file mode 100644 index 000000000..1ea1bc54e --- /dev/null +++ b/core/samples/src/main/java/test/samples/RepositoryFragmentSample.java @@ -0,0 +1,48 @@ +package test.samples; + +import java.util.List; + +public final class RepositoryFragmentSample { + + public interface BaseRepository { + T findById(ID id); + + List findAll(); + } + + public interface ProductRepositoryCustom { + String listByStore(String criteria); + + String getByFriendlyUrl(String url); + } + + public interface ProductRepository extends BaseRepository, ProductRepositoryCustom { + String findBySku(String sku); + } + + public static final class ProductRepositoryImpl implements ProductRepositoryCustom { + @Override + public String listByStore(String criteria) { + sink(criteria); + return criteria; + } + + @Override + public String getByFriendlyUrl(String url) { + return url; + } + } + + private final ProductRepository productRepository; + + public RepositoryFragmentSample(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + public void listProducts(String criteria) { + productRepository.listByStore(criteria); + } + + public static void sink(String query) { + } +} diff --git a/core/samples/src/main/java/test/samples/ShallowRuleSelectionSample.java b/core/samples/src/main/java/test/samples/ShallowRuleSelectionSample.java new file mode 100644 index 000000000..86d7bf4cd --- /dev/null +++ b/core/samples/src/main/java/test/samples/ShallowRuleSelectionSample.java @@ -0,0 +1,77 @@ +package test.samples; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Two independent taint flows that share one sink method. + * + * echo() is a purely local flow that both the field-insensitive shallow pass and the + * field-sensitive full pass discover. + * + * upload()/resync() is the Stirling-PDF shape: one request handler stores request data into a + * Spring singleton (a ClassStatic access path) and a different handler reads it back through a + * getter chain into the sink. + */ +@RestController +public final class ShallowRuleSelectionSample { + @Autowired + private LicenseService licenseService; + + @GetMapping + public void upload(UploadRequest request) { + licenseService.store(request); + } + + @GetMapping + public void resync() { + sink(licenseService.read()); + } + + @GetMapping + public void echo(UploadRequest request) { + sink(request.getName()); + } + + @GetMapping + public void echoSecond(UploadRequest first, UploadRequest second) { + sink(second.getName()); + } + + public static void sink(String path) { + System.out.println(path); + } + + public static class UploadRequest { + private String name; + + public String getName() { + return this.name; + } + } + + public static class LicenseService { + private final Premium premium = new Premium(); + + public void store(UploadRequest request) { + this.premium.setKey(request.getName()); + } + + public String read() { + return this.premium.getKey(); + } + } + + public static class Premium { + private String key; + + public String getKey() { + return this.key; + } + + public void setKey(String key) { + this.key = key; + } + } +} diff --git a/core/samples/src/main/java/test/samples/SpringControllerReturnSinkSample.java b/core/samples/src/main/java/test/samples/SpringControllerReturnSinkSample.java new file mode 100644 index 000000000..704e890c5 --- /dev/null +++ b/core/samples/src/main/java/test/samples/SpringControllerReturnSinkSample.java @@ -0,0 +1,44 @@ +package test.samples; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class SpringControllerReturnSinkSample { + @GetMapping + public String returnDirect(String value) { + return value; + } + + @GetMapping + public String returnStringGetter(Owner owner) { + return owner.getName(); + } + + @GetMapping + public String returnBoxedGetterValueOf(Owner owner) { + return String.valueOf(owner.getId()); + } + + @GetMapping + public String returnBoxedGetterToString(Owner owner) { + return owner.getId().toString(); + } + + public static class BaseEntity { + private Integer id; + + private String name; + + public Integer getId() { + return this.id; + } + + public String getName() { + return this.name; + } + } + + public static final class Owner extends BaseEntity { + } +} diff --git a/core/samples/src/main/java/test/samples/SpringCrossEntryPointSample.java b/core/samples/src/main/java/test/samples/SpringCrossEntryPointSample.java new file mode 100644 index 000000000..a675c3e55 --- /dev/null +++ b/core/samples/src/main/java/test/samples/SpringCrossEntryPointSample.java @@ -0,0 +1,104 @@ +package test.samples; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.Repository; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class SpringCrossEntryPointSample { + @Autowired + private PluginService pluginService; + + @GetMapping + public void uploadNewPlugin(PluginUpload pluginUpload) { + pluginService.savePlugin(pluginUpload); + } + + @GetMapping + public void uploadPlainString(String jarFilePath) { + pluginService.savePlainString(jarFilePath); + } + + @GetMapping + public void deletePlugins() { + pluginService.deletePlugins(); + } + + @GetMapping + public void deletePluginsPlainField() { + pluginService.deletePluginsPlainField(); + } + + @GetMapping + public void uploadAndDeleteRepo(PluginUpload pluginUpload) { + pluginService.savePlugin(pluginUpload); + pluginService.deletePlugins(); + } + + @GetMapping + public void uploadAndDeletePlainField(PluginUpload pluginUpload) { + pluginService.savePlugin(pluginUpload); + pluginService.deletePluginsPlainField(); + } + + public static void sink(String path) { + System.out.println(path); + } + + public static class PluginUpload { + private String jarFile; + + public String getJarFile() { + return this.jarFile; + } + } + + public static class PluginMetadata { + private String jarFilePath; + + public String getJarFilePath() { + return this.jarFilePath; + } + + public void setJarFilePath(String jarFilePath) { + this.jarFilePath = jarFilePath; + } + } + + public interface MetadataDao extends Repository { + PluginMetadata save(PluginMetadata metadata); + + List findAll(); + } + + public static class PluginService { + @Autowired + private MetadataDao metadataDao; + + private String plainField; + + public void savePlugin(PluginUpload pluginUpload) { + PluginMetadata metadata = new PluginMetadata(); + metadata.setJarFilePath(pluginUpload.getJarFile()); + metadataDao.save(metadata); + this.plainField = pluginUpload.getJarFile(); + } + + public void savePlainString(String jarFilePath) { + this.plainField = jarFilePath; + } + + public void deletePlugins() { + for (PluginMetadata metadata : metadataDao.findAll()) { + SpringCrossEntryPointSample.sink(metadata.getJarFilePath()); + } + } + + public void deletePluginsPlainField() { + SpringCrossEntryPointSample.sink(this.plainField); + } + } +} diff --git a/core/samples/src/main/java/test/samples/SpringOverloadedControllerSourceSample.java b/core/samples/src/main/java/test/samples/SpringOverloadedControllerSourceSample.java new file mode 100644 index 000000000..5b1f49927 --- /dev/null +++ b/core/samples/src/main/java/test/samples/SpringOverloadedControllerSourceSample.java @@ -0,0 +1,39 @@ +package test.samples; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class SpringOverloadedControllerSourceSample { + @GetMapping + public void list(FirstRequest request) { + sinkFirst(request.getValue()); + } + + @GetMapping + public void list(SecondRequest request) { + sinkSecond(request.getValue()); + } + + public static void sinkFirst(String value) { + } + + public static void sinkSecond(String value) { + } + + public static final class FirstRequest { + private String value; + + public String getValue() { + return value; + } + } + + public static final class SecondRequest { + private String value; + + public String getValue() { + return value; + } + } +} diff --git a/core/samples/src/main/java/test/samples/SpringRepositoryReturnSinkSample.java b/core/samples/src/main/java/test/samples/SpringRepositoryReturnSinkSample.java new file mode 100644 index 000000000..58bf5f012 --- /dev/null +++ b/core/samples/src/main/java/test/samples/SpringRepositoryReturnSinkSample.java @@ -0,0 +1,48 @@ +package test.samples; + +import org.springframework.data.repository.Repository; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class SpringRepositoryReturnSinkSample { + private final NoteRepository repository; + + public SpringRepositoryReturnSinkSample(NoteRepository repository) { + this.repository = repository; + } + + @GetMapping + public void update(String text) { + StoredNote note = repository.findByUuid("id"); + if (note == null) { + note = new StoredNote(); + } + note.setText(text); + repository.save(note); + } + + @GetMapping + public String render() { + StoredNote note = repository.findByUuid("id"); + return note.getText(); + } + + public interface NoteRepository extends Repository { + StoredNote save(StoredNote note); + + StoredNote findByUuid(String uuid); + } + + public static final class StoredNote { + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } +} diff --git a/core/samples/src/main/java/test/samples/SpringRepositoryStaticFlowSample.java b/core/samples/src/main/java/test/samples/SpringRepositoryStaticFlowSample.java new file mode 100644 index 000000000..118e2b492 --- /dev/null +++ b/core/samples/src/main/java/test/samples/SpringRepositoryStaticFlowSample.java @@ -0,0 +1,51 @@ +package test.samples; + +import org.springframework.data.repository.Repository; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class SpringRepositoryStaticFlowSample { + private final FileRepository repository; + + public SpringRepositoryStaticFlowSample(FileRepository repository) { + this.repository = repository; + } + + @GetMapping + public void update(String path) { + StoredFile file = repository.findByUuid("id"); + if (file == null) { + file = new StoredFile(); + } + file.setPath(path); + repository.save(file); + } + + @GetMapping + public void download() { + StoredFile file = repository.findByUuid("id"); + sink(file.getPath()); + } + + public static void sink(String path) { + } + + public interface FileRepository extends Repository { + StoredFile save(StoredFile file); + + StoredFile findByUuid(String uuid); + } + + public static final class StoredFile { + private String path; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + } +} diff --git a/core/samples/src/main/java/test/samples/StirlingTraceResolutionRegressionPolluter.java b/core/samples/src/main/java/test/samples/StirlingTraceResolutionRegressionPolluter.java new file mode 100644 index 000000000..0278d39a8 --- /dev/null +++ b/core/samples/src/main/java/test/samples/StirlingTraceResolutionRegressionPolluter.java @@ -0,0 +1,24 @@ +package test.samples; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** Adds the shared Spring component state present in the full Stirling application. */ +@RestController +public final class StirlingTraceResolutionRegressionPolluter { + @Autowired private ApplicationProperties applicationProperties; + + @GetMapping + public void pollute(ApplicationProperties request) { + this.applicationProperties = request; + } + + public static final class ApplicationProperties { + private String value; + + public String getValue() { + return value; + } + } +} diff --git a/core/samples/src/main/java/test/samples/StirlingTraceResolutionRegressionSample.java b/core/samples/src/main/java/test/samples/StirlingTraceResolutionRegressionSample.java new file mode 100644 index 000000000..763c69889 --- /dev/null +++ b/core/samples/src/main/java/test/samples/StirlingTraceResolutionRegressionSample.java @@ -0,0 +1,63 @@ +package test.samples; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RestController; + +import stirling.external.StirlingExternal.FileInput; +import stirling.external.StirlingExternal.JsonMapper; +import stirling.external.StirlingExternal.JsonNode; +import stirling.external.StirlingExternal.PdfDocumentFactory; +import test.samples.StirlingTraceResolutionRegressionPolluter.ApplicationProperties; +import test.samples.stirling.common.StirlingWebResponseUtils; + +/** + * Reduction of the Stirling flow: + * Spring component state -> JSON bytes -> WebResponseUtils.bytesToWebResponse + * -> ResponseEntity.Body -> return. + */ +@RestController +public final class StirlingTraceResolutionRegressionSample { + private final PdfDocumentFactory pdfDocumentFactory; + private final ApplicationProperties applicationProperties; + + public StirlingTraceResolutionRegressionSample( + PdfDocumentFactory pdfDocumentFactory, + ApplicationProperties applicationProperties) { + this.pdfDocumentFactory = pdfDocumentFactory; + this.applicationProperties = applicationProperties; + } + + @GetMapping + public ResponseEntity getPdfInfo(@ModelAttribute Request request) throws IOException { + FileInput inputFile = request.getFileInput(); + pdfDocumentFactory.load(inputFile, true); + JsonMapper mapper = new JsonMapper(); + JsonNode jsonOutput = mapper.createObjectNode(); + JsonNode metadata = mapper.createObjectNode(); + metadata.put("Title", applicationProperties.getValue()); + jsonOutput.set("Metadata", metadata); + JsonNode basicInfo = mapper.createObjectNode(); + String fileSizeInBytes = inputFile.getSize(); + basicInfo.put("FileSizeInBytes", fileSizeInBytes); + jsonOutput.set("BasicInfo", basicInfo); + String jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput); + return StirlingWebResponseUtils.bytesToWebResponse( + jsonString.getBytes(StandardCharsets.UTF_8), + "response.json", + MediaType.APPLICATION_JSON); + } + + public static final class Request { + private FileInput fileInput; + + public FileInput getFileInput() { + return fileInput; + } + } +} diff --git a/core/samples/src/main/java/test/samples/ThingsBoardEntityActionExplosionSample.java b/core/samples/src/main/java/test/samples/ThingsBoardEntityActionExplosionSample.java new file mode 100644 index 000000000..c8e0dca0d --- /dev/null +++ b/core/samples/src/main/java/test/samples/ThingsBoardEntityActionExplosionSample.java @@ -0,0 +1,335 @@ +package test.samples; + +public class ThingsBoardEntityActionExplosionSample { + private static String source() { + return "tainted"; + } + + private static void sink(String value) { + } + + public static void entityActionExplosion(int entityKind, int action) { + String tainted = source(); + Object[] additionalInfo = new Object[]{tainted, tainted, tainted}; + switch (entityKind) { + case 0: + pushEntityActionToRuleEngine( + new AssetId(tainted), new Asset(tainted), new AdminUser(tainted), action, additionalInfo); + break; + case 1: + pushEntityActionToRuleEngine( + new DeviceId(tainted), new Device(tainted), new CustomerUser(tainted), action, additionalInfo); + break; + case 2: + pushEntityActionToRuleEngine( + new DashboardId(tainted), new Dashboard(tainted), new AdminUser(tainted), action, additionalInfo); + break; + case 3: + pushEntityActionToRuleEngine( + new RuleChainId(tainted), new RuleChain(tainted), new CustomerUser(tainted), action, additionalInfo); + break; + case 4: + pushEntityActionToRuleEngine( + new AssetId(tainted), new Asset(tainted), new CustomerUser(tainted), action, additionalInfo); + break; + default: + pushEntityActionToRuleEngine( + new DeviceId(tainted), new Device(tainted), new AdminUser(tainted), action, additionalInfo); + break; + } + } + + public static void singleEntityAction(int action) { + String tainted = source(); + pushEntityActionToRuleEngine( + new AssetId(tainted), + new Asset(tainted), + new AdminUser(tainted), + action, + new Object[]{tainted, tainted, tainted}); + } + + public static void classStaticContextExplosion() { + Object value = source(); + seedClassStatic(value); + classStaticHotMethod(new SafeContextA()); + classStaticHotMethod(new SafeContextB()); + classStaticHotMethod(new SafeContextC()); + classStaticHotMethod(new SafeContextD()); + classStaticHotMethod(new SafeContextE()); + classStaticHotMethod(new SafeContextF()); + } + + public static void singleClassStaticContext() { + Object value = source(); + seedClassStatic(value); + classStaticHotMethod(new SafeContextA()); + } + + private static void seedClassStatic(Object value) { + } + + private static void classStaticHotMethod(Context context) { + if (context != null) { + Object first = new Object(); + Object second = new Object(); + if (first != second) { + first = second; + } + } + classStaticSink(); + } + + private static void classStaticSink() { + } + + public static void controlOnlyFanout(int selector) { + String value = source(); + if (selector == 0) { + // control-only branch + } else if (selector == 1) { + // control-only branch + } else if (selector == 2) { + // control-only branch + } else if (selector == 3) { + // control-only branch + } else if (selector == 4) { + // control-only branch + } else if (selector == 5) { + // control-only branch + } else if (selector == 6) { + // control-only branch + } else if (selector == 7) { + // control-only branch + } + sink(value); + } + + public static void contextSupportedSideEffectBatch() { + String tainted = source(); + safeContextSink(processContext(new SafeContextA(), tainted)); + safeContextSink(processContext(new SafeContextB(), tainted)); + safeContextSink(processContext(new SafeContextC(), tainted)); + safeContextSink(processContext(new SafeContextD(), tainted)); + safeContextSink(processContext(new SafeContextE(), tainted)); + safeContextSink(processContext(new SafeContextF(), tainted)); + taintedContextSink(processContext(new TaintedContext(), tainted)); + } + + public static void singleContextSupportedSideEffect() { + taintedContextSink(processContext(new TaintedContext(), source())); + } + + private static String processContext(Context context, String value) { + ContextBox box = new ContextBox(); + storeContextValue(box, value); + return context.select(box.value); + } + + private static void storeContextValue(ContextBox box, String value) { + box.value = value; + } + + private static void safeContextSink(String value) { + } + + private static void taintedContextSink(String value) { + } + + private static void pushEntityActionToRuleEngine( + EntityId entityId, + HasName entity, + User user, + int action, + Object... additionalInfo) { + MetaData metaData = new MetaData(); + if (user != null) { + metaData.putValue("userId", user.getId()); + metaData.putValue("userName", user.getName()); + metaData.putValue("userEmail", user.getEmail()); + if (user.getFirstName() != null) { + metaData.putValue("userFirstName", user.getFirstName()); + } + if (user.getLastName() != null) { + metaData.putValue("userLastName", user.getLastName()); + } + } + + if (action == 0) { + metaData.putValue("assignedCustomerId", extractParameter(String.class, 0, additionalInfo)); + metaData.putValue("assignedCustomerName", extractParameter(String.class, 1, additionalInfo)); + } else if (action == 1) { + metaData.putValue("unassignedCustomerId", extractParameter(String.class, 0, additionalInfo)); + metaData.putValue("unassignedCustomerName", extractParameter(String.class, 1, additionalInfo)); + } else if (action == 2) { + metaData.putValue("assignedTenantId", extractParameter(String.class, 0, additionalInfo)); + metaData.putValue("assignedTenantName", extractParameter(String.class, 1, additionalInfo)); + } else if (action == 3) { + metaData.putValue("assignedEdgeId", extractParameter(String.class, 0, additionalInfo)); + metaData.putValue("assignedEdgeName", extractParameter(String.class, 1, additionalInfo)); + } else if (action == 4) { + metaData.putValue("comment", extractParameter(String.class, 0, additionalInfo)); + } + + EntityNode entityNode = new EntityNode(); + if (entity != null) { + entityNode.put("entityName", entity.getName()); + entityNode.put("entityType", entityId.getEntityType()); + if (action == 5) { + entityNode.put("attributeScope", extractParameter(String.class, 0, additionalInfo)); + entityNode.put("attributeValue", extractParameter(String.class, 1, additionalInfo)); + } else if (action == 6) { + entityNode.put("timeseriesKey", extractParameter(String.class, 0, additionalInfo)); + entityNode.put("timeseriesValue", extractParameter(String.class, 1, additionalInfo)); + } else if (action == 7) { + entityNode.put("relation", extractParameter(String.class, 2, additionalInfo)); + } + } + + sink(metaData.value); + sink(entityNode.value); + } + + private static T extractParameter(Class type, int index, Object... additionalInfo) { + if (additionalInfo != null && additionalInfo.length > index) { + Object value = additionalInfo[index]; + if (type.isInstance(value)) { + return type.cast(value); + } + } + return null; + } + + private interface EntityId { + String getEntityType(); + } + + private interface HasName { + String getName(); + } + + private interface User { + String getId(); + String getName(); + String getEmail(); + String getFirstName(); + String getLastName(); + } + + private abstract static class ValueHolder { + final String value; + + ValueHolder(String value) { + this.value = value; + } + } + + private static final class AssetId extends ValueHolder implements EntityId { + AssetId(String value) { super(value); } + public String getEntityType() { return value; } + } + + private static final class DeviceId extends ValueHolder implements EntityId { + DeviceId(String value) { super(value); } + public String getEntityType() { return value; } + } + + private static final class DashboardId extends ValueHolder implements EntityId { + DashboardId(String value) { super(value); } + public String getEntityType() { return value; } + } + + private static final class RuleChainId extends ValueHolder implements EntityId { + RuleChainId(String value) { super(value); } + public String getEntityType() { return value; } + } + + private static final class Asset extends ValueHolder implements HasName { + Asset(String value) { super(value); } + public String getName() { return value; } + } + + private static final class Device extends ValueHolder implements HasName { + Device(String value) { super(value); } + public String getName() { return value; } + } + + private static final class Dashboard extends ValueHolder implements HasName { + Dashboard(String value) { super(value); } + public String getName() { return value; } + } + + private static final class RuleChain extends ValueHolder implements HasName { + RuleChain(String value) { super(value); } + public String getName() { return value; } + } + + private abstract static class BaseUser extends ValueHolder implements User { + BaseUser(String value) { super(value); } + public String getId() { return value; } + public String getName() { return value; } + public String getEmail() { return value; } + public String getFirstName() { return value; } + public String getLastName() { return value; } + } + + private static final class AdminUser extends BaseUser { + AdminUser(String value) { super(value); } + } + + private static final class CustomerUser extends BaseUser { + CustomerUser(String value) { super(value); } + } + + private static final class MetaData { + String value; + + void putValue(String key, String value) { + this.value = value; + } + } + + private static final class EntityNode { + String value; + + void put(String key, String value) { + this.value = value; + } + } + + private interface Context { + String select(String value); + } + + private static final class SafeContextA implements Context { + public String select(String value) { return "safe-a"; } + } + + private static final class SafeContextB implements Context { + public String select(String value) { return "safe-b"; } + } + + private static final class SafeContextC implements Context { + public String select(String value) { return "safe-c"; } + } + + private static final class SafeContextD implements Context { + public String select(String value) { return "safe-d"; } + } + + private static final class SafeContextE implements Context { + public String select(String value) { return "safe-e"; } + } + + private static final class SafeContextF implements Context { + public String select(String value) { return "safe-f"; } + } + + private static final class TaintedContext implements Context { + public String select(String value) { return value; } + } + + private static final class ContextBox { + String value; + } +} diff --git a/core/samples/src/main/java/test/samples/ThreadStaticFieldSample.java b/core/samples/src/main/java/test/samples/ThreadStaticFieldSample.java new file mode 100644 index 000000000..aa2618541 --- /dev/null +++ b/core/samples/src/main/java/test/samples/ThreadStaticFieldSample.java @@ -0,0 +1,101 @@ +package test.samples; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class ThreadStaticFieldSample { + @GetMapping + public void exportViaThread(String surveyId) { + Holder.savePath = surveyId; + new Thread(new AnswerTask()).start(); + } + + @GetMapping + public void exportViaDirectCall(String surveyId) { + Holder.savePath = surveyId; + new ExcelWorker().run(); + } + + @GetMapping + public void exportViaThreadSubclass(String surveyId) { + Holder.savePath = surveyId; + new ExcelWorker().start(); + } + + @GetMapping + public void exportViaInterface(String surveyId) { + Holder.savePath = surveyId; + Runnable task = pick(); + task.run(); + } + + @GetMapping + public void exportViaHelper(String surveyId) { + Holder.savePath = surveyId; + runIt(pick()); + } + + @GetMapping + public void exportInstanceViaThreadSubclass(String surveyId) { + ExcelWorker worker = new ExcelWorker(); + worker.instancePath = surveyId; + worker.start(); + } + + @GetMapping + public void exportInstanceViaDirectCall(String surveyId) { + ExcelWorker worker = new ExcelWorker(); + worker.instancePath = surveyId; + worker.run(); + } + + @GetMapping + public void exportViaThreadSubclassNoStatic(String surveyId) { + Holder.savePath = surveyId; + new PlainWorker().start(); + } + + private static Runnable pick() { + if (System.currentTimeMillis() > 0) { + return new AnswerTask(); + } + return new ExcelWorker(); + } + + private static void runIt(Runnable task) { + task.run(); + } + + public static void sink(String path) { + System.out.println(path); + } + + public static final class Holder { + public static String savePath; + } + + public static final class AnswerTask implements Runnable { + @Override + public void run() { + System.out.println("unrelated"); + } + } + + public static class ExcelWorker extends Thread { + public String instancePath; + + @Override + public void run() { + ThreadStaticFieldSample.sink(Holder.savePath); + ThreadStaticFieldSample.sink(this.instancePath); + } + } + + public static final class PlainWorker extends Thread { + @Override + public void run() { + ThreadStaticFieldSample.sink(Holder.savePath); + } + } +} diff --git a/core/samples/src/main/java/test/samples/TracePremiseCartesianSample.java b/core/samples/src/main/java/test/samples/TracePremiseCartesianSample.java new file mode 100644 index 000000000..a79dcd3a5 --- /dev/null +++ b/core/samples/src/main/java/test/samples/TracePremiseCartesianSample.java @@ -0,0 +1,102 @@ +package test.samples; + +public class TracePremiseCartesianSample { + private static void sink(String value) { + } + + public static void entryOne(String first, String second, boolean chooseFirst) { + multipleOriginsOne(first, second, chooseFirst); + } + + private static void multipleOriginsOne(String first, String second, boolean chooseFirst) { + String selected = chooseFirst ? first : second; + consumeOne(selected); + } + + private static void consumeOne(String selected) { + sink(selected); + } + + public static void entry( + String firstLeft, + String secondLeft, + String firstRight, + String secondRight, + boolean chooseLeft, + boolean chooseRight) { + multipleOrigins(firstLeft, secondLeft, firstRight, secondRight, chooseLeft, chooseRight); + } + + private static void multipleOrigins( + String firstLeft, + String secondLeft, + String firstRight, + String secondRight, + boolean chooseLeft, + boolean chooseRight) { + String left; + if (chooseLeft) { + left = firstLeft; + } else { + left = secondLeft; + } + + String right; + if (chooseRight) { + right = firstRight; + } else { + right = secondRight; + } + + consume(left, right); + } + + private static void consume(String left, String right) { + sink(left); + sink(right); + } + + public static void entryThree( + String firstLeft, + String secondLeft, + String firstMiddle, + String secondMiddle, + String firstRight, + String secondRight, + boolean chooseLeft, + boolean chooseMiddle, + boolean chooseRight) { + multipleOriginsThree( + firstLeft, + secondLeft, + firstMiddle, + secondMiddle, + firstRight, + secondRight, + chooseLeft, + chooseMiddle, + chooseRight); + } + + private static void multipleOriginsThree( + String firstLeft, + String secondLeft, + String firstMiddle, + String secondMiddle, + String firstRight, + String secondRight, + boolean chooseLeft, + boolean chooseMiddle, + boolean chooseRight) { + String left = chooseLeft ? firstLeft : secondLeft; + String middle = chooseMiddle ? firstMiddle : secondMiddle; + String right = chooseRight ? firstRight : secondRight; + consumeThree(left, middle, right); + } + + private static void consumeThree(String left, String middle, String right) { + sink(left); + sink(middle); + sink(right); + } +} diff --git a/core/samples/src/main/java/test/samples/stirling/common/StirlingWebResponseUtils.java b/core/samples/src/main/java/test/samples/stirling/common/StirlingWebResponseUtils.java new file mode 100644 index 000000000..71ad6b098 --- /dev/null +++ b/core/samples/src/main/java/test/samples/stirling/common/StirlingWebResponseUtils.java @@ -0,0 +1,25 @@ +package test.samples.stirling.common; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +public final class StirlingWebResponseUtils { + private StirlingWebResponseUtils() { } + + public static ResponseEntity bytesToWebResponse( + byte[] bytes, String documentName, MediaType mediaType) throws IOException { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + headers.setContentLength(bytes.length); + String encodedDocumentName = + URLEncoder.encode(documentName, StandardCharsets.UTF_8).replace("+", "%20"); + headers.setContentDispositionFormData("attachment", encodedDocumentName); + return new ResponseEntity<>(bytes, headers, HttpStatus.OK); + } +} diff --git a/core/settings.gradle.kts b/core/settings.gradle.kts index a2517a8fd..e0ea26d06 100644 --- a/core/settings.gradle.kts +++ b/core/settings.gradle.kts @@ -8,6 +8,7 @@ include("opentaint-java-querylang") include("opentaint-java-querylang:samples") include("opentaint-go-querylang") include("samples") +include("samples-dependency") fun DependencySubstitutions.substituteProjects(group: String, projects: List) { for (projectName in projects) { diff --git a/core/src/main/kotlin/org/opentaint/common/sast/rules/SemgrepRuleProvider.kt b/core/src/main/kotlin/org/opentaint/common/sast/rules/SemgrepRuleProvider.kt index b9d947152..c6b834cab 100644 --- a/core/src/main/kotlin/org/opentaint/common/sast/rules/SemgrepRuleProvider.kt +++ b/core/src/main/kotlin/org/opentaint/common/sast/rules/SemgrepRuleProvider.kt @@ -11,12 +11,14 @@ abstract class SemgrepRuleProvider( private var ruleIdFilter: Set? = null fun selectRelevantSemgrepRules(ruleIds: Set) { - ruleIdFilter = rules - .flatMapTo(hashSetOf()) { rule -> reduce(rule.root, ruleIds).retainedRuleIds } + ruleIdFilter = relevantSemgrepRuleIds(ruleIds) logger.debug { "Select ${ruleIdFilter?.size} from ${rules.sumOf { it.size }} rules" } } + fun relevantSemgrepRuleIds(candidateRuleIds: Set): Set = rules + .flatMapTo(hashSetOf()) { rule -> reduce(rule.root, candidateRuleIds).retainedRuleIds } + private data class Reduction( val applicable: Boolean, val retainedRuleIds: Set, diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringWebProject.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringWebProject.kt index 1ebbef45c..568794bd1 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringWebProject.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringWebProject.kt @@ -739,7 +739,7 @@ private class SpringControllerEntryPointGenerator( val epReturnType = PredefinedPrimitives.Void.typeName() val entryPointMethod = SpringGeneratedMethod( - name = controller.name, + name = controller.springEntryPointName(), returnType = epReturnType, description = methodDescription(emptyList(), epReturnType), parameters = emptyList(), @@ -1043,6 +1043,20 @@ private class SpringControllerEntryPointGenerator( } } +private fun JIRMethod.springEntryPointName(): String { + val overloadSignatures = enclosingClass.declaredMethods + .asSequence() + .filter { it.name == name } + .map { it.description } + .sorted() + .toList() + if (overloadSignatures.size == 1) return name + + val overloadIndex = overloadSignatures.binarySearch(description) + check(overloadIndex >= 0) { "Method signature not found in its declaring class: $this" } + return "$name\$opentaint\$$overloadIndex" +} + private fun generateStubValue(type: JIRType): JIRValue? = when (type) { is JIRPrimitiveType -> when (type.typeName) { PredefinedPrimitives.Boolean -> JIRBool(true, type) diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/rules/JIRSemgrepRuleProvider.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/rules/JIRSemgrepRuleProvider.kt index 7016c49d1..4dca0e34a 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/rules/JIRSemgrepRuleProvider.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/rules/JIRSemgrepRuleProvider.kt @@ -47,6 +47,9 @@ class JIRSemgrepRuleProvider( selectRelevantSemgrepRules(ruleIds) } + override fun relevantRuleIds(candidateRuleIds: Set): Set = + relevantSemgrepRuleIds(candidateRuleIds) + override fun entryPointRulesForMethod( method: CommonMethod, statement: CommonInst, diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt index 0b6fa495d..944b09704 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt @@ -125,4 +125,4 @@ abstract class AbstractAnalyzerRunner : CliWithLogger() { companion object { private val logger = object : KLogging() {}.logger } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/org/opentaint/common/sast/rules/SemgrepRuleProviderTest.kt b/core/src/test/kotlin/org/opentaint/common/sast/rules/SemgrepRuleProviderTest.kt new file mode 100644 index 000000000..2d1c6bced --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/common/sast/rules/SemgrepRuleProviderTest.kt @@ -0,0 +1,78 @@ +package org.opentaint.common.sast.rules + +import org.junit.jupiter.api.Test +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep.Structure +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep.TaintRuleGroup +import kotlin.test.assertEquals + +class SemgrepRuleProviderTest { + @Test + fun `relevant rule ids retain only candidate graph with selected sink`() { + val provider = TestProvider( + listOf( + taintRule("first", source = "source-a", sink = "sink-a"), + taintRule("second", source = "source-b", sink = "sink-b"), + ) + ) + + val relevant = provider.relevantSemgrepRuleIds( + setOf("source-a", "source-b", "sink-a") + ) + + assertEquals(setOf("source-a", "sink-a"), relevant) + } + + @Test + fun `relevant rule ids retain dependency chain`() { + val sourceGroup = TaintRuleGroup( + rules = listOf("source", "source-dependent"), + ruleDependencies = mapOf("source-dependent" to setOf("source")), + finalRuleIds = setOf("source-dependent"), + ) + val sinkGroup = TaintRuleGroup( + rules = listOf("sink"), + finalRuleIds = setOf("sink"), + ) + val provider = TestProvider( + listOf( + TaintRuleFromSemgrep( + ruleId = "dependent", + root = Structure.Taint( + sources = listOf(sourceGroup), + sinks = listOf(sinkGroup), + propagators = emptyList(), + sanitizers = emptyList(), + ), + ) + ) + ) + + val relevant = provider.relevantSemgrepRuleIds( + setOf("source", "source-dependent", "sink") + ) + + assertEquals(setOf("source", "source-dependent", "sink"), relevant) + } + + private class TestProvider( + rules: List>, + ) : SemgrepRuleProvider(rules) { + override fun String.ruleItemId(): String = this + override fun String.resolvedRuleId(): String = this + } + + private fun taintRule( + id: String, + source: String, + sink: String, + ) = TaintRuleFromSemgrep( + ruleId = id, + root = Structure.Taint( + sources = listOf(TaintRuleGroup(listOf(source), finalRuleIds = setOf(source))), + sinks = listOf(TaintRuleGroup(listOf(sink), finalRuleIds = setOf(sink))), + propagators = emptyList(), + sanitizers = emptyList(), + ), + ) +} diff --git a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/AnalysisTest.kt b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/AnalysisTest.kt index 0dc2beaef..6e1508fa1 100644 --- a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/AnalysisTest.kt +++ b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/AnalysisTest.kt @@ -4,6 +4,7 @@ import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance import org.opentaint.common.sast.CommonAnalysisOptions +import org.opentaint.dataflow.ap.ifds.access.ApMode import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace import org.opentaint.dataflow.configuration.go.serialized.GoNameMatcher @@ -184,7 +185,8 @@ abstract class AnalysisTest { loadedConfig.loadConfig(serializedConfig) val options = CommonAnalysisOptions( - ifdsAnalysisTimeout = 1.minutes + ifdsAnalysisTimeout = 1.minutes, + ifdsApMode = ApMode.Tree, ) val analyzer = GoTaintAnalyzer(cp, loadedConfig, GoTestUnitResolver, options.taintAnalyzerOptions()) analyzer.use { diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt index b87bdd735..6f51bd940 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt @@ -13,6 +13,7 @@ import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFunctionNameMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule @@ -67,6 +68,20 @@ abstract class AnalysisTest : BasicTestUtils() { ) ) + fun wholeObjectSourceRule(fqn: String, methodName: String, taintMark: String): SerializedRule.Source = + SerializedRule.Source( + function = functionMatcher(fqn, methodName), + taint = listOf( + SerializedTaintAssignAction( + kind = taintMark, + pos = PositionBaseWithModifiers.WithModifiers( + PositionBase.Result, + listOf(PositionModifier.AnyField), + ), + ) + ), + ) + fun entryPointRule(fqn: String, methodName: String, taintMark: String, argIndex: Int) = SerializedRule.EntryPoint( function = functionMatcher(fqn, methodName), @@ -106,6 +121,12 @@ abstract class AnalysisTest : BasicTestUtils() { } open val useDefaultConfig = false + open val useDefaultUnrollStrategy = false + + open fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = rulesProvider + + open fun unitResolver(projectLocation: RegisteredLocation): JIRUnitResolver = + SingleLocationUnit(projectLocation) private class SingleLocationUnit(val loc: RegisteredLocation) : JIRUnitResolver { override fun resolve(method: JIRMethod): UnitType { @@ -126,7 +147,15 @@ abstract class AnalysisTest : BasicTestUtils() { fun runAnalysis( config: SerializedTaintConfig, entryPointClass: String, - entryPointMethod: String + entryPointMethod: String, + apMode: ApMode = ApMode.Tree, + shallowApMode: ApMode = ApMode.BaseOnlyField, + afterTraceAnalysis: (( + List, + TaintAnalyzer, + JIRSafeApplicationGraph, + ) -> Unit)? = null, + afterAnalysis: ((TaintAnalyzer, JIRSafeApplicationGraph) -> Unit)? = null, ): List { val cls = cp.findClassOrNull(entryPointClass) ?: error("Class $entryPointClass not found in CP") val ep = cls.declaredMethods.singleOrNull { it.name == entryPointMethod } @@ -142,27 +171,37 @@ abstract class AnalysisTest : BasicTestUtils() { var rulesProvider: TaintRulesProvider = JIRTaintRulesProvider(taintConfig) rulesProvider = JIRMethodExitRuleProvider(rulesProvider) + rulesProvider = customizeRulesProvider(rulesProvider) val usages = runBlocking { cp.usagesExt() } val mainGraph = JApplicationGraphImpl(cp, usages) - val ifdsGraph = JIRSafeApplicationGraph(mainGraph) + val tryBoundaryExceptionsGraph = JTryBoundaryExceptionsApplicationGraph(mainGraph) + val ifdsGraph = JIRSafeApplicationGraph(tryBoundaryExceptionsGraph) val options = TaintAnalyzerOptions( ifdsTimeout = 1.minutes, - ifdsApMode = ApMode.Tree + ifdsApMode = apMode, + shallowScanApMode = shallowApMode, ) val analyzer = object : TaintAnalyzer(options) { override val unrollStrategy: AnyAccessorUnrollStrategy - get() = AnyAccessorUnrollStrategy.AnyAccessorDisabled + get() = if (useDefaultUnrollStrategy) { + super.unrollStrategy + } else { + AnyAccessorUnrollStrategy.AnyAccessorDisabled + } override fun analysisGraph(): ApplicationGraph = ifdsGraph override fun analysisManager() = JIRAnalysisManager(cp, refManager, rulesProvider) - override fun unitResolver() = SingleLocationUnit(cls.declaration.location) + override fun unitResolver() = this@AnalysisTest.unitResolver(cls.declaration.location) } return analyzer.use { - it.analyzeWithIfds(listOf(ep)).first + val result = it.analyzeWithIfds(listOf(ep)).first + afterTraceAnalysis?.invoke(result, it, ifdsGraph) + afterAnalysis?.invoke(it, ifdsGraph) + result } } @@ -172,8 +211,9 @@ abstract class AnalysisTest : BasicTestUtils() { entryPointName: String, ruleId: String, testName: String, + apMode: ApMode = ApMode.Tree, ) { - val traces = runAnalysis(config, testCls, entryPointName) + val traces = runAnalysis(config, testCls, entryPointName, apMode) assertTrue(traces.isNotEmpty(), "$testName: expected taint to reach the sink, but no vulnerability was found") traces.forEach { vt -> assertEquals( @@ -188,8 +228,9 @@ abstract class AnalysisTest : BasicTestUtils() { testCls: String, entryPointName: String, testName: String, + apMode: ApMode = ApMode.Tree, ) { - val traces = runAnalysis(config, testCls, entryPointName) + val traces = runAnalysis(config, testCls, entryPointName, apMode) assertTrue(traces.isEmpty(), "$testName: expected no vulnerability, but found ${traces.size}") } } diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceInstallFuzzTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceInstallFuzzTest.kt new file mode 100644 index 000000000..8f52f4969 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceInstallFuzzTest.kt @@ -0,0 +1,35 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +class BaseOnlyReferenceInstallFuzzTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + private val testClass = "test.samples.BaseOnlyReferenceInstallFuzzSample" + private val ruleId = "baseonly-reference-install-fuzz" + private val mark = "reference-install-taint" + private val config = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))), + ) + + @TestFactory + fun `Tree reference installations must survive BaseOnly summaries`(): List = + samples.map { method -> + DynamicTest.dynamicTest(method) { + assertReachable(config, testClass, method, ruleId, "$method Tree control", ApMode.Tree) + assertReachable(config, testClass, method, ruleId, "$method BaseOnly regression", ApMode.BaseOnlyField) + } + } + + private companion object { + val samples = listOf( + "constructorInstallThroughEnvelope", + "constructorInstallThroughTwoEnvelopes", + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceMutationFuzzTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceMutationFuzzTest.kt new file mode 100644 index 000000000..93b54436f --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceMutationFuzzTest.kt @@ -0,0 +1,123 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +class BaseOnlyReferenceMutationFuzzTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + private val testClass = "test.samples.BaseOnlyReferenceMutationFuzzSample" + private val ruleId = "baseonly-reference-mutation-fuzz" + private val mark = "reference-mutation-taint" + private val config = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))), + ) + + @TestFactory + fun `Tree reference flows must also survive BaseOnly summaries`(): List = + samples.map { method -> + DynamicTest.dynamicTest(method) { + assertReachable(config, testClass, method, ruleId, "$method Tree control", ApMode.Tree) + assertReachable(config, testClass, method, ruleId, "$method BaseOnly regression", ApMode.BaseOnlyField) + } + } + + private companion object { + val samples = listOf( + "holderConstructorAfterTaint", + "namedHolderConstructorAfterTaint", + "holderFactoryAfterTaint", + "namedHolderFactoryAfterTaint", + "envelopeConstructorAfterTaint", + "pairConstructorFirstArgument", + "pairConstructorSecondArgument", + "assignBoxThroughHolderSetter", + "holderConstructorAliasArgument", + "holderConstructorLocalAlias", + "holderConstructorCastArgument", + "holderConstructorNullMetadata", + "holderConstructorIdentityMetadata", + "tripleConstructorFirstArgument", + "tripleConstructorMiddleArgument", + "tripleConstructorLastArgument", + "tripleFactoryFirstArgument", + "pairFactoryFirstArgument", + "pairFactorySecondArgument", + "holderFactoryAliasedArgument", + "holderFactoryCastArgument", + "nestedHolderFactory", + "doubleEnvelopeConstructor", + "envelopeWithMetadataConstructor", + "holderSetterViaHelper", + "holderSetterWithMetadataViaHelper", + "holderSetterAliasArgument", + "holderSetterCastArgument", + "holderOverwriteSafeThenTainted", + "holderOverwriteTaintedTwice", + "alternateHolderPrimaryField", + "alternateHolderSecondaryField", + "alternateConstructorPrimaryField", + "alternateConstructorSecondaryField", + "alternateOverwritePrimaryField", + "alternateOverwriteSecondaryField", + "namedHolderSetBoxAndName", + "namedHolderSetBoxAndNullName", + "pairThenEnvelopeFirstField", + "pairThenEnvelopeSecondField", + "quadConstructorFirstArgument", + "quadConstructorSecondArgument", + "quadConstructorThirdArgument", + "quadConstructorFourthArgument", + "quadFactoryFirstArgument", + "quadFactorySecondArgument", + "quadFactoryThirdArgument", + "quadFactoryFourthArgument", + "keyedConstructorLeftField", + "keyedConstructorCenterField", + "keyedConstructorRightField", + "keyedSetterLeftField", + "keyedSetterCenterField", + "keyedSetterRightField", + "alternatePrimaryViaHelper", + "alternateSecondaryViaHelper", + "alternatePrimaryViaNestedHelper", + "alternatePrimaryViaFactory", + "alternatePrimaryNullThenTainted", + "alternateSecondaryNullThenTainted", + "alternatePrimarySafeThenTaintedViaHelper", + "holderNullThenTainted", + "holderTaintedNullThenTainted", + "quadEnvelopeFirstField", + "quadEnvelopeFourthField", + "alternateEnvelopePrimaryField", + "alternateEnvelopeSecondaryField", + "alternateSetBothFirstArgument", + "alternateSetBothSecondArgument", + "keyedSetAllCenterArgument", + "quadConstructorFirstWithNullPeers", + "quadConstructorSecondWithNullPeers", + "quadConstructorThirdWithNullPeers", + "quadConstructorFourthWithNullPeers", + "keyedConstructorLeftWithNullPeers", + "keyedConstructorCenterWithNullPeers", + "keyedConstructorRightWithNullPeers", + "keyedEnvelopeLeftField", + "keyedEnvelopeCenterField", + "keyedEnvelopeRightField", + "doubleAlternateEnvelopePrimaryField", + "doubleAlternateEnvelopeSecondaryField", + "keyedOverwriteLeftSafeThenTainted", + "keyedOverwriteCenterSafeThenTainted", + "keyedOverwriteRightSafeThenTainted", + "alternateSetBothFirstWithNullPeer", + "alternateSetBothSecondWithNullPeer", + "keyedFactoryLeftField", + "keyedFactoryCenterField", + "alternateConstructorPrimaryWithNullPeer", + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceTransferFuzzTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceTransferFuzzTest.kt new file mode 100644 index 000000000..38c12ea6b --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyReferenceTransferFuzzTest.kt @@ -0,0 +1,34 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +class BaseOnlyReferenceTransferFuzzTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + private val testClass = "test.samples.BaseOnlyReferenceTransferFuzzSample" + private val ruleId = "base-only-reference-transfer-fuzz" + private val mark = "reference-transfer-source" + private val config = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))), + ) + + private val cases = listOf( + "nestedEnvelopeConstructors", "tripleNestedConstructors", "envelopeFactory", + "envelopeFactoryFromValue", "outerFactoryFromValue", "envelopeSetterAfterPayloadConstructor", + "envelopeSetterAfterPayloadSetter", "fluentNestedEnvelope", + "referenceArrayWrapper", + ) + + @TestFactory + fun `Tree reference transfers omitted by BaseOnly forward analysis`() = cases.map { method -> + DynamicTest.dynamicTest(method) { + assertReachable(config, testClass, method, ruleId, "$method Tree control", ApMode.Tree) + assertReachable(config, testClass, method, ruleId, "$method BaseOnlyField forward regression", ApMode.BaseOnlyField) + } + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyShallowReturnSinkTraceTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyShallowReturnSinkTraceTest.kt new file mode 100644 index 000000000..4776593d0 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyShallowReturnSinkTraceTest.kt @@ -0,0 +1,54 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcher +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcherDispatchMethod +import org.opentaint.jvm.sast.project.spring.SpringRuleProvider + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class BaseOnlyShallowReturnSinkTraceTest : AnalysisTest() { + companion object { + private const val SAMPLE_CLASS = "test.samples.SpringRepositoryReturnSinkSample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "spring-repository-return-sink" + } + + override val sourceFileExtension: String = "java" + + override val useDefaultUnrollStrategy: Boolean = true + + override fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = + SpringRuleProvider(rulesProvider, checkNotNull(context.springWebProjectContext)) + + private fun analyze(shallowApMode: ApMode): Set { + val config = SerializedTaintConfig( + entryPoint = listOf(entryPointRule(SAMPLE_CLASS, "update", TAINT_MARK, argIndex = 0)), + methodExitSink = listOf(methodExitSinkRule(SAMPLE_CLASS, "render", RULE_ID, TAINT_MARK)), + ) + + val traces = runAnalysis( + config = config, + entryPointClass = GeneratedSpringControllerDispatcher, + entryPointMethod = GeneratedSpringControllerDispatcherDispatchMethod, + apMode = ApMode.Tree, + shallowApMode = shallowApMode, + ) + + return traces.mapTo(hashSetOf()) { it.vulnerability.rule.id } + } + + @Test + fun `tree shallow scan resolves the method exit sink trace`() { + assertEquals(setOf(RULE_ID), analyze(ApMode.Tree)) + } + + @Test + fun `base only shallow scan resolves the method exit sink trace`() { + assertEquals(setOf(RULE_ID), analyze(ApMode.BaseOnlyField)) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyShallowTraceResolutionTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyShallowTraceResolutionTest.kt new file mode 100644 index 000000000..70157ccb3 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyShallowTraceResolutionTest.kt @@ -0,0 +1,55 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcher +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcherDispatchMethod +import org.opentaint.jvm.sast.project.spring.SpringRuleProvider + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class BaseOnlyShallowTraceResolutionTest : AnalysisTest() { + companion object { + private const val SAMPLE_CLASS = "test.samples.SpringRepositoryStaticFlowSample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "spring-repository-static-flow" + } + + override val sourceFileExtension: String = "java" + + override val useDefaultUnrollStrategy: Boolean = true + + override fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = + SpringRuleProvider(rulesProvider, checkNotNull(context.springWebProjectContext)) + + private fun analyze(shallowApMode: ApMode): Set { + val config = SerializedTaintConfig( + entryPoint = listOf(entryPointRule(SAMPLE_CLASS, "update", TAINT_MARK, argIndex = 0)), + sink = listOf(sinkRule(SAMPLE_CLASS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))), + ) + + val traces = runAnalysis( + config = config, + entryPointClass = GeneratedSpringControllerDispatcher, + entryPointMethod = GeneratedSpringControllerDispatcherDispatchMethod, + apMode = ApMode.Tree, + shallowApMode = shallowApMode, + ) + + return traces.mapTo(hashSetOf()) { it.vulnerability.rule.id } + } + + @Test + fun `tree shallow scan resolves the class-static repository trace`() { + assertEquals(setOf(RULE_ID), analyze(ApMode.Tree)) + } + + @Test + fun `base only shallow scan resolves the class-static repository trace`() { + assertEquals(setOf(RULE_ID), analyze(ApMode.BaseOnlyField)) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlySummaryFieldExplosionTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlySummaryFieldExplosionTest.kt new file mode 100644 index 000000000..7d82c1706 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlySummaryFieldExplosionTest.kt @@ -0,0 +1,79 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace +import org.opentaint.dataflow.ap.ifds.trace.path.ResolvedInterProceduralTrace +import org.opentaint.dataflow.ap.ifds.trace.path.ResolvedInterProceduralTraceEntry +import org.opentaint.dataflow.ap.ifds.trace.path.TracePathGenerationResult +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +class BaseOnlySummaryFieldExplosionTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + private val testClass = "test.samples.BaseOnlySummaryFieldExplosionSample" + private val ruleId = "baseonly-summary-field-explosion" + private val mark = "summary-field-explosion-taint" + private val config = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))), + ) + + @Test + fun `field enumeration preserves the trace witness in both ap modes`() { + val treeVulnerabilities = runAnalysis( + config = config, + entryPointClass = testClass, + entryPointMethod = "fieldEnumerationExplosion", + apMode = ApMode.Tree, + ) + assertResolvedHelperTrace(treeVulnerabilities, "Tree") + + val baseOnlyVulnerabilities = runAnalysis( + config = config, + entryPointClass = testClass, + entryPointMethod = "fieldEnumerationExplosion", + apMode = ApMode.BaseOnlyField, + ) + assertResolvedHelperTrace(baseOnlyVulnerabilities, "BaseOnly") + } + + @Test + fun `irrelevant call retains the exact source-to-sink premises`() { + val vulnerabilities = runAnalysis( + config = config, + entryPointClass = testClass, + entryPointMethod = "irrelevantCallConclusionSharing", + apMode = ApMode.BaseOnlyField, + ) + + assertTrue(vulnerabilities.isNotEmpty(), "the exact source-to-sink premises must be retained") + } + + private fun assertResolvedHelperTrace( + vulnerabilities: List, + mode: String, + ) { + assertTrue(vulnerabilities.isNotEmpty(), "$mode must preserve the source-to-sink flow") + val paths = vulnerabilities.mapNotNull { it.trace as? TracePathGenerationResult.Path } + assertTrue(paths.isNotEmpty(), "$mode must resolve a complete trace path") + assertTrue( + paths.any { path -> + path.path.any { node -> + (node.root2Source + node.root2SinkNoRoot).any { it.containsMethod("permuteField") } + } + }, + "$mode trace must resolve the generalized permuteField summary", + ) + } + + private fun ResolvedInterProceduralTrace.containsMethod(name: String): Boolean { + if (method.method.name == name) return true + return entries.any { entry -> + entry is ResolvedInterProceduralTraceEntry.InnerCall && entry.innerTrace.containsMethod(name) + } + } + +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceProjectionFuzzTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceProjectionFuzzTest.kt new file mode 100644 index 000000000..4cec04722 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceProjectionFuzzTest.kt @@ -0,0 +1,38 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +class BaseOnlyTraceProjectionFuzzTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + override val useDefaultUnrollStrategy: Boolean = true + + private val testClass = "test.samples.BaseOnlyTraceProjectionFuzzSample" + private val ruleId = "base-only-trace-projection-fuzz" + private val mark = "trace-projection-taint" + private val config = SerializedTaintConfig( + source = listOf(wholeObjectSourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))), + ) + + @TestFactory + fun `Tree traces rejected by BaseOnly projection trace resolution`(): List = + samples.map { method -> + DynamicTest.dynamicTest(method) { + assertReachable(config, testClass, method, ruleId, "$method Tree control", ApMode.Tree) + assertReachable(config, testClass, method, ruleId, "$method BaseOnly trace candidate", ApMode.BaseOnlyField) + } + } + + private companion object { + val samples = listOf( + "projectOneLevel", + "projectThreeLevels", + "relayThenProject", + "mutateThenProject", + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceResolutionFuzzTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceResolutionFuzzTest.kt new file mode 100644 index 000000000..3a65dbbbe --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceResolutionFuzzTest.kt @@ -0,0 +1,39 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +class BaseOnlyTraceResolutionFuzzTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + private val testClass = "test.samples.BaseOnlyTraceRelayFuzzSample" + private val ruleId = "base-only-trace-resolution-fuzz" + private val mark = "trace-resolution-taint" + private val config = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))), + ) + + @TestFactory + fun `Tree traces rejected by BaseOnly trace resolution`(): List = + samples.map { method -> + DynamicTest.dynamicTest(method) { + assertReachable(config, testClass, method, ruleId, "$method Tree control", ApMode.Tree) + assertReachable(config, testClass, method, ruleId, "$method BaseOnly trace regression", ApMode.BaseOnlyField) + } + } + + private companion object { + val samples = listOf( + "returnThroughIdentity", + "returnThroughDoubleIdentity", + "returnThroughInstanceIdentity", + "returnThroughInterfaceIdentity", + "returnThroughBranchIdentity", + "returnOuterThroughIdentity", + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceShapeFuzzTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceShapeFuzzTest.kt new file mode 100644 index 000000000..ffe966284 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/BaseOnlyTraceShapeFuzzTest.kt @@ -0,0 +1,74 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +class BaseOnlyTraceShapeFuzzTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + override val useDefaultUnrollStrategy: Boolean = true + + private val testClass = "test.samples.BaseOnlyTraceShapeFuzzSample" + private val ruleId = "base-only-trace-shape-fuzz" + private val mark = "trace-shape-taint" + private val probeMark = "trace-shape-probe" + private val probeResultMark = "trace-shape-probe-result" + + private val config = SerializedTaintConfig( + source = listOf( + sourceRule(testClass, "source", mark), + sourceRule(testClass, "source", probeMark), + SerializedRule.Source( + function = functionMatcher(testClass, "probe"), + condition = SerializedCondition.ContainsMark( + probeMark, + PositionBaseWithModifiers.BaseOnly(Argument(0)), + ), + taint = listOf( + SerializedTaintAssignAction( + kind = probeResultMark, + pos = PositionBaseWithModifiers.BaseOnly(PositionBase.Result), + ), + ), + ), + ), + sink = listOf( + SerializedRule.Sink( + function = functionMatcher(testClass, "sink"), + condition = SerializedCondition.ContainsMark(mark, responseBody(Argument(0))), + id = ruleId, + ), + ), + ) + + @TestFactory + fun `BaseOnly resolves traces through field abstract summaries`(): List = + samples.map { method -> + DynamicTest.dynamicTest(method) { + assertReachable(config, testClass, method, ruleId, "$method Tree control", ApMode.Tree) + assertReachable(config, testClass, method, ruleId, "$method BaseOnly", ApMode.BaseOnlyField) + } + } + + private fun responseBody(position: PositionBase) = PositionBaseWithModifiers.WithModifiers( + position, + listOf(PositionModifier.Field("$testClass\$Response", "body", "$testClass\$Token")), + ) + + private companion object { + val samples = listOf( + "projectedProbedFactory", + "projectedProbedConstructorFactory", + "projectedDoubleProbedFactory", + "projectedRelayedProbeFactory", + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/JavaDataFlowReachabilityTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/JavaDataFlowReachabilityTest.kt index 785dafed0..33a971e2a 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/JavaDataFlowReachabilityTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/JavaDataFlowReachabilityTest.kt @@ -3,7 +3,12 @@ package org.opentaint.jvm.sast.dataflow import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.access.ApMode import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.ClassStatic +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -19,6 +24,9 @@ class JavaDataFlowReachabilityTest : AnalysisTest() { private const val OPTIONAL_RULE_ID = "optional-flow-rule" private const val STREAM_RULE_ID = "stream-flow-rule" private const val ASYNC_RULE_ID = "async-flow-rule" + private const val BASE_ONLY_SETTER_RULE_ID = "base-only-setter-regression" + private const val BASE_ONLY_NESTED_REFERENCE_RULE_ID = "base-only-nested-reference-regression" + private const val BASE_ONLY_TRACE_RESOLUTION_RULE_ID = "base-only-trace-resolution-regression" } override val sourceFileExtension: String = "java" @@ -41,6 +49,155 @@ class JavaDataFlowReachabilityTest : AnalysisTest() { ) } + @Test + fun `base-only class-static fact follows a transitive rule footprint`() { + val testCls = "$SAMPLE_PACKAGE.BaseOnlyClassStaticFootprintSample" + val state = ClassStatic("test.class-static-footprint") + val config = classStaticFootprintConfig(testCls, state) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "transitiveRuleFootprint", + ruleId = "class-static-footprint-rule", + testName = "transitive class-static footprint", + apMode = ApMode.BaseOnly, + ) + } + + private fun classStaticFootprintConfig( + testCls: String, + state: ClassStatic, + ): SerializedTaintConfig = + SerializedTaintConfig( + source = listOf( + sourceRule(testCls, "source", TAINT_MARK), + SerializedRule.Source( + function = functionMatcher(testCls, "seed"), + condition = listOf(Argument(0) to TAINT_MARK).condition(), + taint = listOf( + SerializedTaintAssignAction( + kind = "ready", + pos = PositionBaseWithModifiers.BaseOnly(state), + ) + ), + ), + SerializedRule.Source( + function = functionMatcher(testCls, "transition"), + condition = listOf( + Argument(0) to TAINT_MARK, + state to "ready", + ).condition(), + taint = listOf( + SerializedTaintAssignAction( + kind = "advanced", + pos = PositionBaseWithModifiers.BaseOnly(state), + ) + ), + ), + ), + sink = listOf( + sinkRule( + testCls, + "sink", + "class-static-footprint-rule", + listOf(Argument(0) to TAINT_MARK, state to "advanced"), + ) + ), + ) + + @Test + fun `virtual dispatch - override cache is scoped by constrained base class`() { + val testCls = "$SAMPLE_PACKAGE.MethodOverridesCacheSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule(testCls, "sink", "override-cache-rule", listOf(Argument(0) to TAINT_MARK)) + ), + ) + + assertNotReachable( + config = config, + testCls = testCls, + entryPointName = "narrowCallMustNotReuseBroadOverrides", + testName = "override cache base-class constraint", + ) + } + + @Test + fun `virtual dispatch - incompatible generic bridge cannot return normally`() { + val testCls = "$SAMPLE_PACKAGE.GenericBridgeDispatchSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule(testCls, "sink", "generic-bridge-rule", listOf(Argument(0) to TAINT_MARK)) + ), + ) + + assertNotReachable( + config = config, + testCls = testCls, + entryPointName = "incompatibleBridgeMustNotReturn", + testName = "incompatible generic bridge", + ) + } + + @Test + fun `virtual dispatch - compatible generic bridge remains reachable`() { + val testCls = "$SAMPLE_PACKAGE.GenericBridgeDispatchSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule(testCls, "sink", "generic-bridge-rule", listOf(Argument(0) to TAINT_MARK)) + ), + ) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "compatibleBridgeMustReach", + ruleId = "generic-bridge-rule", + testName = "compatible generic bridge", + ) + } + + @Test + fun `virtual dispatch - Object declared method remains ignored after receiver refinement`() { + val testCls = "$SAMPLE_PACKAGE.ObjectMethodDispatchSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule(testCls, "sink", "object-method-rule", listOf(Argument(0) to TAINT_MARK)) + ), + ) + + assertNotReachable( + config = config, + testCls = testCls, + entryPointName = "callThroughObjectMustBeIgnored", + testName = "Object-declared virtual call remains ignored", + ) + } + + @Test + fun `virtual dispatch - directly declared override remains analyzable`() { + val testCls = "$SAMPLE_PACKAGE.ObjectMethodDispatchSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule(testCls, "sink", "object-method-rule", listOf(Argument(0) to TAINT_MARK)) + ), + ) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "directOverrideCallRemainsAnalyzable", + ruleId = "object-method-rule", + testName = "direct Object override call", + ) + } + @Test fun `field flow - source to sink through single field`() { val testCls = "$SAMPLE_PACKAGE.FieldFlowSample" @@ -74,6 +231,74 @@ class JavaDataFlowReachabilityTest : AnalysisTest() { ) } + @Test + fun `base-only flow - tainted field survives an unrelated setter`() { + val testCls = "$SAMPLE_PACKAGE.KkFileViewSetterIdentityRegressionSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule(testCls, "sink", BASE_ONLY_SETTER_RULE_ID, listOf(Argument(0) to TAINT_MARK)) + ) + ) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "taintedLocalSurvivesUnrelatedSetters", + ruleId = BASE_ONLY_SETTER_RULE_ID, + testName = "BaseOnly unrelated setter regression" + ) + } + + @Test + fun `base-only flow - tainted child survives installation into an outer field`() { + val testCls = "$SAMPLE_PACKAGE.BaseOnlyNestedReferenceRegressionSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule( + testCls, + "sink", + BASE_ONLY_NESTED_REFERENCE_RULE_ID, + listOf(Argument(0) to TAINT_MARK), + ) + ) + ) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "nestedReferenceFlow", + ruleId = BASE_ONLY_NESTED_REFERENCE_RULE_ID, + testName = "BaseOnly nested reference installation regression" + ) + } + + @Test + fun `base-only flow - trace resolves through nested factory result`() { + val testCls = "$SAMPLE_PACKAGE.BaseOnlyTraceResolutionFuzzSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf( + sinkRule( + testCls, + "sink", + BASE_ONLY_TRACE_RESOLUTION_RULE_ID, + listOf(Argument(0) to TAINT_MARK), + ) + ), + ) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "nestedFactory", + ruleId = BASE_ONLY_TRACE_RESOLUTION_RULE_ID, + testName = "BaseOnly nested factory trace resolution", + apMode = ApMode.BaseOnlyField, + ) + } + @Test fun `interprocedural flow - source to sink through chained methods`() { val testCls = "$SAMPLE_PACKAGE.InterproceduralDataFlowSample" @@ -91,6 +316,44 @@ class JavaDataFlowReachabilityTest : AnalysisTest() { ) } + @Test + fun `over-approximate start trace - non-zero summary starts at method entry`() { + val testCls = "$SAMPLE_PACKAGE.OverApproximateStartTraceSample" + val ruleId = "over-approximate-non-zero-start" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf(sinkRule(testCls, "sink", ruleId, listOf(Argument(0) to TAINT_MARK))), + ) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "nonZeroSummary", + ruleId = ruleId, + testName = "non-Zero summary direct MethodEntry", + apMode = ApMode.BaseOnlyField, + ) + } + + @Test + fun `over-approximate start trace - first zero origin on every CFG branch is retained`() { + val testCls = "$SAMPLE_PACKAGE.OverApproximateStartTraceSample" + val ruleId = "over-approximate-zero-frontier" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", TAINT_MARK)), + sink = listOf(sinkRule(testCls, "sink", ruleId, listOf(Argument(0) to TAINT_MARK))), + ) + + assertReachable( + config = config, + testCls = testCls, + entryPointName = "zeroSummary", + ruleId = ruleId, + testName = "Zero summary CFG origin frontier", + apMode = ApMode.BaseOnlyField, + ) + } + @Test fun `branch flow - source to sink through conditional branches`() { val testCls = "$SAMPLE_PACKAGE.BranchLoopDataFlowSample" diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/KkFileViewSetterIdentityRegressionTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/KkFileViewSetterIdentityRegressionTest.kt new file mode 100644 index 000000000..8d30ce487 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/KkFileViewSetterIdentityRegressionTest.kt @@ -0,0 +1,41 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +@Disabled("Moved to JavaDataFlowReachabilityTest") +class KkFileViewSetterIdentityRegressionTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + @Test + fun `tainted whole local survives unrelated setters and reaches sink through field getter`() { + val testClass = "test.samples.KkFileViewSetterIdentityRegressionSample" + val ruleId = "kkfileview-setter-identity-regression" + val mark = "kkfileview-untrusted-path" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))) + ) + + assertReachable( + config = config, + testCls = testClass, + entryPointName = "taintedLocalSurvivesUnrelatedSetters", + ruleId = ruleId, + testName = "kkFileView setter identity Tree control", + apMode = ApMode.Tree, + ) + + assertReachable( + config = config, + testCls = testClass, + entryPointName = "taintedLocalSurvivesUnrelatedSetters", + ruleId = ruleId, + testName = "kkFileView setter identity BaseOnly regression", + apMode = ApMode.BaseOnlyField, + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/RepositoryFragmentCallResolutionTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/RepositoryFragmentCallResolutionTest.kt new file mode 100644 index 000000000..bf92fd85d --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/RepositoryFragmentCallResolutionTest.kt @@ -0,0 +1,65 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class RepositoryFragmentCallResolutionTest : AnalysisTest() { + + companion object { + private const val SAMPLE_CLASS = "test.samples.RepositoryFragmentSample" + private const val LIBRARY_SAMPLE_CLASS = "test.samples.LibraryFragmentSample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "repository-fragment-flow" + } + + override val sourceFileExtension: String = "java" + + private val config = SerializedTaintConfig( + entryPoint = listOf(entryPointRule(SAMPLE_CLASS, "listProducts", TAINT_MARK, argIndex = 0)), + sink = listOf(sinkRule(SAMPLE_CLASS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))), + ) + + private val libraryConfig = SerializedTaintConfig( + entryPoint = listOf(entryPointRule(LIBRARY_SAMPLE_CLASS, "listProducts", TAINT_MARK, argIndex = 0)), + sink = listOf(sinkRule(LIBRARY_SAMPLE_CLASS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))), + ) + + @Test + fun `fragment implementation is reachable through repository typed receiver with Tree`() { + assertReachable( + config = config, + testCls = SAMPLE_CLASS, + entryPointName = "listProducts", + ruleId = RULE_ID, + testName = "repository fragment (Tree)", + apMode = ApMode.Tree, + ) + } + + @Test + fun `fragment implementation is reachable through repository typed receiver with BaseOnlyField`() { + assertReachable( + config = config, + testCls = SAMPLE_CLASS, + entryPointName = "listProducts", + ruleId = RULE_ID, + testName = "repository fragment (BaseOnlyField)", + apMode = ApMode.BaseOnlyField, + ) + } + + @Test + fun `fragment declared outside the project is deliberately not widened`() { + assertNotReachable( + config = libraryConfig, + testCls = LIBRARY_SAMPLE_CLASS, + entryPointName = "listProducts", + testName = "library fragment widening guard", + apMode = ApMode.Tree, + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ResolvedRuleSharingTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ResolvedRuleSharingTest.kt new file mode 100644 index 000000000..20c4a4629 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ResolvedRuleSharingTest.kt @@ -0,0 +1,89 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Test +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFunctionNameMatcher +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SinkMetaData +import org.opentaint.ir.api.jvm.JIRMethod +import org.opentaint.jvm.sast.ast.BasicTestUtils +import org.opentaint.jvm.sast.dataflow.rules.TaintConfiguration +import kotlin.test.assertEquals +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class ResolvedRuleSharingTest : BasicTestUtils() { + override val sourceFileExtension: String get() = "java" + + private fun anyName() = SerializedSimpleNameMatcher.Pattern(".*") + + private fun anyFunction() = + SerializedFunctionNameMatcher.Complex(anyName(), anyName(), anyName()) + + private fun exitSinkRule() = SerializedRule.MethodExitSink( + function = anyFunction(), + condition = SerializedCondition.ContainsMark( + tainted = "shared-test-mark", + pos = PositionBaseWithModifiers.BaseOnly(PositionBase.Result), + ), + id = "shared-test-sink", + meta = SinkMetaData(cwe = listOf(79), note = "shared meta"), + ) + + private fun stringMethods(): Pair { + val stringClass = findClass("java.lang.String") + val trim = stringClass.declaredMethods.single { it.name == "trim" && it.parameters.isEmpty() } + val toLower = stringClass.declaredMethods.single { it.name == "toLowerCase" && it.parameters.isEmpty() } + return trim to toLower + } + + @Test + fun `resolved rules for distinct methods share condition, meta and actions`() { + val configuration = TaintConfiguration(cp) + configuration.loadConfig(SerializedTaintConfig(methodExitSink = listOf(exitSinkRule()))) + + val (first, second) = stringMethods() + + val firstRules = configuration.methodExitSinkForMethod(first, allRelevant = false) + val secondRules = configuration.methodExitSinkForMethod(second, allRelevant = false) + + assertEquals(1, firstRules.size) + assertEquals(1, secondRules.size) + + val a = firstRules.single() + val b = secondRules.single() + + assertTrue(a.method !== b.method) + assertSame(a.condition, b.condition) + assertSame(a.meta, b.meta) + assertEquals(a.id, b.id) + } + + @Test + fun `sharing preserves resolved rule values`() { + val rule = exitSinkRule() + + val shared = TaintConfiguration(cp) + shared.loadConfig(SerializedTaintConfig(methodExitSink = listOf(rule))) + + val (first, second) = stringMethods() + + val sharedFirst = shared.methodExitSinkForMethod(first, allRelevant = false).single() + + val isolated = TaintConfiguration(cp) + isolated.loadConfig(SerializedTaintConfig(methodExitSink = listOf(rule))) + val isolatedFirst = isolated.methodExitSinkForMethod(first, allRelevant = false).single() + + assertEquals(isolatedFirst, sharedFirst) + assertEquals(isolatedFirst.condition, sharedFirst.condition) + assertEquals(isolatedFirst.meta, sharedFirst.meta) + + val sharedSecond = shared.methodExitSinkForMethod(second, allRelevant = false).single() + assertEquals(second, sharedSecond.method) + assertEquals(isolatedFirst.condition, sharedSecond.condition) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ShallowRuleSelectionNarrowingTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ShallowRuleSelectionNarrowingTest.kt new file mode 100644 index 000000000..5e5a9f093 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ShallowRuleSelectionNarrowingTest.kt @@ -0,0 +1,247 @@ +package org.opentaint.jvm.sast.dataflow + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.common.sast.dataflow.TaintAnalyzer +import org.opentaint.common.sast.dataflow.TaintAnalyzerOptions +import org.opentaint.config.JavaDefaultConfigLoader +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.MethodWithContext +import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager +import org.opentaint.dataflow.ap.ifds.TaintAnalysisUnitRunnerManager +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy.AnyAccessorDisabled +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.ap.ifds.taint.TaintSinkTracker +import org.opentaint.dataflow.ap.ifds.trace.ExactProcessingTimeBudget +import org.opentaint.dataflow.ap.ifds.trace.TraceResolver +import org.opentaint.dataflow.ap.ifds.trace.action.ActionableRulesCollectionResult +import org.opentaint.dataflow.ap.ifds.trace.action.mergeActionableRules +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.jvm.TaintEntryPointSource +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.ifds.SingletonUnit +import org.opentaint.dataflow.ifds.UnitType +import org.opentaint.dataflow.ifds.UnknownUnit +import org.opentaint.dataflow.jvm.ap.ifds.JIRSafeApplicationGraph +import org.opentaint.dataflow.jvm.ap.ifds.analysis.JIRAnalysisManager +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.dataflow.jvm.ifds.JIRUnitResolver +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.jvm.JIRMethod +import org.opentaint.ir.api.jvm.RegisteredLocation +import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.impl.features.usagesExt +import org.opentaint.jvm.graph.JApplicationGraphImpl +import org.opentaint.jvm.sast.dataflow.rules.TaintConfiguration +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcher +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcherDispatchMethod +import org.opentaint.jvm.sast.project.spring.SpringRuleProvider +import org.opentaint.util.analysis.ApplicationGraph +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +private typealias SelectedRules = Map>> + +/** + * Pins the recall cost of deriving the full scan's rule set from the shallow scan. + * + * The staged pipeline resolves traces for the field-insensitive shallow discoveries, collects the + * rules those traces visited, and then restricts the field-sensitive full scan to exactly that set + * ([JIRAnalysisManager.selectPhase], `Phase.FullScan -> phaseTaintConfig.select(actionableRules)`). + * A flow whose real path needs a source statement the shallow traces never visited therefore cannot + * be reported, even though the full scan alone finds it. + * + * The restricted and unrestricted arms both end in a field-sensitive Tree pass. The decisive + * comparison is between two `Phase.FullScan` arms that differ only in two map entries, so no + * phase-dependent behaviour other than rule selection can explain the difference. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ShallowRuleSelectionNarrowingTest : AnalysisTest() { + companion object { + private const val SAMPLE_CLASS = "test.samples.ShallowRuleSelectionSample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "shallow-rule-selection" + } + + override val sourceFileExtension: String = "java" + + override fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = + SpringRuleProvider(rulesProvider, checkNotNull(context.springWebProjectContext)) + + private fun multiArgEntryPointRule(methodName: String, vararg argIndices: Int) = + SerializedRule.EntryPoint( + function = functionMatcher(SAMPLE_CLASS, methodName), + taint = argIndices.map { idx -> + SerializedTaintAssignAction( + kind = TAINT_MARK, + pos = PositionBaseWithModifiers.BaseOnly(Argument(idx)), + ) + }, + ) + + private val config + get() = SerializedTaintConfig( + entryPoint = listOf( + entryPointRule(SAMPLE_CLASS, "upload", TAINT_MARK, argIndex = 0), + entryPointRule(SAMPLE_CLASS, "echo", TAINT_MARK, argIndex = 0), + ), + sink = listOf(sinkRule(SAMPLE_CLASS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))), + ) + + private data class Run( + val shallowDiscoveries: Set, + val selection: SelectedRules, + val fullScanDiscoveries: Set, + val vulnerabilityStatements: Map, + ) + + @Test + fun `a source rule keeps only the actions the shallow trace needed`() { + val twoArgConfig = SerializedTaintConfig( + entryPoint = listOf(multiArgEntryPointRule("echoSecond", 0, 1)), + sink = listOf(sinkRule(SAMPLE_CLASS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))), + ) + + val run = runPipeline(restrictFullScan = true, config = twoArgConfig) + assertEquals(setOf("echoSecond"), run.fullScanDiscoveries) + + val narrowed = run.selection.values + .flatMap { it.entries } + .filter { (rule, _) -> rule is TaintEntryPointSource } + .map { (rule, actions) -> (rule as TaintEntryPointSource).actionsAfter.size to actions.size } + + assertEquals( + listOf(4 to 2), narrowed, + "the Spring entry point rule carries one action per argument, doubled by " + + "SpringRuleProvider.taintObjectFields into {arg, arg.*}; only the two actions on the " + + "argument the shallow trace walked survive relevantActions()", + ) + } + + private fun runPipeline( + restrictFullScan: Boolean, + config: SerializedTaintConfig = this.config, + extraRules: (TaintRulesProvider) -> SelectedRules = { emptyMap() }, + ): Run { + val dispatcher = checkNotNull(cp.findClassOrNull(GeneratedSpringControllerDispatcher)) + .declaredMethods.single { it.name == GeneratedSpringControllerDispatcherDispatchMethod } + + val taintConfig = TaintConfiguration(cp).also { it.loadConfig(config) } + JavaDefaultConfigLoader.loadConfig()?.let { defaults -> + taintConfig.loadConfig(SerializedTaintConfig(passThrough = defaults.passThrough)) + } + + var rulesProvider: TaintRulesProvider = JIRTaintRulesProvider(taintConfig) + rulesProvider = JIRMethodExitRuleProvider(rulesProvider) + rulesProvider = customizeRulesProvider(rulesProvider) + + val usages = runBlocking { cp.usagesExt() } + val graph = JIRSafeApplicationGraph( + JTryBoundaryExceptionsApplicationGraph(JApplicationGraphImpl(cp, usages)), + ) + val projectLocation = dispatcher.enclosingClass.declaration.location + val unitResolver = object : JIRUnitResolver { + override fun resolve(method: JIRMethod): UnitType = + if (method.enclosingClass.declaration.location == projectLocation || + DataFlowApproximationLoader.isApproximation(method) + ) SingletonUnit else UnknownUnit + + override fun locationIsUnknown(loc: RegisteredLocation): Boolean = loc != projectLocation + } + + val managerHolder = arrayOfNulls(1) + val analyzer = object : TaintAnalyzer( + TaintAnalyzerOptions(ifdsTimeout = 2.minutes, ifdsApMode = ApMode.Tree), + ) { + override fun analysisGraph(): ApplicationGraph = graph + override fun analysisManager(): JIRAnalysisManager = + JIRAnalysisManager(cp, refManager, rulesProvider).also { managerHolder[0] = it } + + override fun unitResolver(): JIRUnitResolver = unitResolver + } + + return analyzer.use { + val engine = it.ifdsEngine + val manager = checkNotNull(managerHolder[0]) + val startMethods = listOf(MethodWithContext(dispatcher, EmptyMethodContext)) + val entryPoints = setOf(dispatcher) + + manager.selectPhase(TaintAnalysisManager.Phase.Prescan) + engine.resetApManager(TreeApManager(AnyAccessorDisabled, it.refManager, it.cancellation)) + engine.runAnalysis(startMethods, timeout = 1.minutes, cancellationTimeout = 30.seconds) + + val shallowManager = BaseOnlyApManager(it.unrollStrategy, it.cancellation, fieldSensitive = true) + manager.selectPhase(TaintAnalysisManager.Phase.ShallowScan) + engine.resetApManager(shallowManager) + engine.runAnalysis(startMethods, timeout = 1.minutes, cancellationTimeout = 30.seconds) + engine.cleanup() + + val shallowVulnerabilities = engine.confirmVulnerabilities( + entryPoints, engine.getVulnerabilities(), 1.minutes, cancellationTimeout = 30.seconds, + ) + val selection = collectRules(engine, shallowManager, entryPoints, shallowVulnerabilities) + + val installed = selection + extraRules(rulesProvider) + manager.selectPhase( + if (restrictFullScan) { + TaintAnalysisManager.Phase.FullScan(installed) + } else { + TaintAnalysisManager.Phase.ShallowScan + } + ) + engine.resetApManager(TreeApManager(it.unrollStrategy, it.refManager, it.cancellation)) + engine.runAnalysis(startMethods, timeout = 1.minutes, cancellationTimeout = 30.seconds) + engine.cleanup() + + val found = engine.getVulnerabilities() + Run( + shallowDiscoveries = shallowVulnerabilities.mapTo(hashSetOf()) { v -> + v.statement.location.method.name + }, + selection = selection, + fullScanDiscoveries = found.mapTo(hashSetOf()) { v -> v.statement.location.method.name }, + vulnerabilityStatements = found.associateBy({ v -> v.statement.location.method.name }) { v -> + v.statement + }, + ) + } + } + + private fun collectRules( + engine: TaintAnalysisUnitRunnerManager, + shallowManager: BaseOnlyApManager, + entryPoints: Set, + vulnerabilities: List, + ): SelectedRules { + if (vulnerabilities.isEmpty()) return emptyMap() + shallowManager.enableTraceResolutionMode() + val budget = ExactProcessingTimeBudget(10.seconds) + val interProceduralTraces = engine.resolveVulnerabilityInterProceduralTraces( + entryPoints, vulnerabilities, + resolverParams = TraceResolver.Params( + resolveEntryPointToStartTrace = false, + resolveAllTraces = true, + ), + timeout = 1.minutes, + cancellationTimeout = 30.seconds, + exactTimeBudget = budget, + ) + val results = engine.resolveVulnerabilityActionableRules( + interProceduralTraces, timeout = 1.minutes, cancellationTimeout = 30.seconds, + exactTimeBudget = budget, + ) + return mergeActionableRules(results.filterIsInstance()) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ShallowScanRecallTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ShallowScanRecallTest.kt new file mode 100644 index 000000000..814a5c5ea --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ShallowScanRecallTest.kt @@ -0,0 +1,97 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcher +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcherDispatchMethod +import org.opentaint.jvm.sast.project.spring.SpringRuleProvider + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ShallowScanRecallTest : AnalysisTest() { + companion object { + private const val TAINT_MARK = "tainted" + } + + override val sourceFileExtension: String = "java" + override val useDefaultUnrollStrategy: Boolean = true + override val useDefaultConfig: Boolean = true + + override fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = + SpringRuleProvider(rulesProvider, checkNotNull(context.springWebProjectContext)) + + private fun probe( + sampleClass: String, + entryPoints: List>, + ruleId: String, + shallowApMode: ApMode, + ): Set { + val config = SerializedTaintConfig( + entryPoint = entryPoints.map { (m, idx) -> entryPointRule(sampleClass, m, TAINT_MARK, argIndex = idx) }, + sink = listOf(sinkRule(sampleClass, "sink", ruleId, listOf(Argument(0) to TAINT_MARK))), + ) + + val traces = runAnalysis( + config = config, + entryPointClass = GeneratedSpringControllerDispatcher, + entryPointMethod = GeneratedSpringControllerDispatcherDispatchMethod, + apMode = ApMode.Tree, + shallowApMode = shallowApMode, + ) + return traces.mapTo(hashSetOf()) { it.vulnerability.rule.id } + } + + private fun assertShallowModesAgree(name: String, sampleClass: String, entryPoints: List>) { + val ruleId = "diag-$name" + val tree = probe(sampleClass, entryPoints, ruleId, ApMode.Tree) + val baseOnlyField = probe(sampleClass, entryPoints, ruleId, ApMode.BaseOnlyField) + assertEquals(setOf(ruleId), tree, "$name: shallow=Tree lost the flow") + assertEquals(tree, baseOnlyField, "$name: shallow=BaseOnlyField lost the flow that shallow=Tree finds") + } + + @Test + fun `taint stored into a spring bean field survives the shallow scan`() { + val cls = "test.samples.SpringCrossEntryPointSample" + assertShallowModesAgree("A-plain-same", cls, listOf("uploadAndDeletePlainField" to 0)) + } + + @Test + fun `taint stored into a spring repository survives the shallow scan across entry points`() { + val cls = "test.samples.SpringCrossEntryPointSample" + assertShallowModesAgree("A-repo-cross", cls, listOf("uploadNewPlugin" to 0)) + } + + @Test + fun `taint on a static field survives a dispatch made one frame below the write`() { + val cls = "test.samples.ThreadStaticFieldSample" + assertShallowModesAgree("C-helper", cls, listOf("exportViaHelper" to 0)) + } + + @Test + fun `taint on a static field survives Thread start`() { + val cls = "test.samples.ThreadStaticFieldSample" + assertShallowModesAgree("C-subclass", cls, listOf("exportViaThreadSubclass" to 0)) + } + + @Test + fun `taint on a static field survives a dispatch made in the writing frame`() { + val cls = "test.samples.ThreadStaticFieldSample" + assertShallowModesAgree("C-iface", cls, listOf("exportViaInterface" to 0)) + } + + @Test + fun `whole object seed reaches a sink through a collection element getter`() { + val cls = "test.samples.CollectionElementGetterSample" + assertShallowModesAgree("B-list", cls, listOf("searchViaList" to 0)) + } + + @Test + fun `whole object seed reaches a sink through the shopizer getter chain`() { + val cls = "test.samples.CollectionElementGetterSample" + assertShallowModesAgree("B-shopizer", cls, listOf("searchShopizerShape" to 1)) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringControllerReturnSinkTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringControllerReturnSinkTest.kt new file mode 100644 index 000000000..7fd736046 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringControllerReturnSinkTest.kt @@ -0,0 +1,52 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcher +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcherDispatchMethod +import org.opentaint.jvm.sast.project.spring.SpringRuleProvider + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SpringControllerReturnSinkTest : AnalysisTest() { + companion object { + private const val SAMPLE_CLASS = "test.samples.SpringControllerReturnSinkSample" + private const val TAINT_MARK = "tainted" + } + + override val sourceFileExtension: String = "java" + override val useDefaultUnrollStrategy: Boolean = true + override val useDefaultConfig: Boolean = true + + override fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = + SpringRuleProvider(rulesProvider, checkNotNull(context.springWebProjectContext)) + + private fun assertSinkReached(method: String) { + val ruleId = "spring-return-$method" + val config = SerializedTaintConfig( + entryPoint = listOf(entryPointRule(SAMPLE_CLASS, method, TAINT_MARK, argIndex = 0)), + methodExitSink = listOf(methodExitSinkRule(SAMPLE_CLASS, method, ruleId, TAINT_MARK)), + ) + + val traces = runAnalysis( + config = config, + entryPointClass = GeneratedSpringControllerDispatcher, + entryPointMethod = GeneratedSpringControllerDispatcherDispatchMethod, + ) + + assertEquals(setOf(ruleId), traces.mapTo(hashSetOf()) { it.vulnerability.rule.id }) + } + + @Test + fun `tainted argument returned directly reaches the method exit sink`() { + assertSinkReached("returnDirect") + } + + @Test + fun `tainted bean read through a String getter reaches the method exit sink`() { + assertSinkReached("returnStringGetter") + } + +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringOverloadedControllerSourceTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringOverloadedControllerSourceTest.kt new file mode 100644 index 000000000..a05a506a0 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringOverloadedControllerSourceTest.kt @@ -0,0 +1,51 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcher +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcherDispatchMethod +import org.opentaint.jvm.sast.project.spring.SpringRuleProvider + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SpringOverloadedControllerSourceTest : AnalysisTest() { + companion object { + private const val SAMPLE_CLASS = "test.samples.SpringOverloadedControllerSourceSample" + private const val TAINT_MARK = "tainted" + private const val FIRST_RULE_ID = "spring-overload-first" + private const val SECOND_RULE_ID = "spring-overload-second" + } + + override val sourceFileExtension: String = "java" + + override fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = + SpringRuleProvider(rulesProvider, checkNotNull(context.springWebProjectContext)) + + @Test + fun `all overloaded Spring controller methods are seeded during shallow analysis`() { + val generatedWrapperClass = findClass("${SAMPLE_CLASS}_Opentaint_EntryPoint") + val overloadWrappers = generatedWrapperClass.declaredMethods.filter { it.name.startsWith("list") } + assertEquals(2, overloadWrappers.mapTo(hashSetOf()) { it.name }.size) + + val config = SerializedTaintConfig( + entryPoint = listOf(entryPointRule(SAMPLE_CLASS, "list", TAINT_MARK, argIndex = 0)), + sink = listOf( + sinkRule(SAMPLE_CLASS, "sinkFirst", FIRST_RULE_ID, listOf(Argument(0) to TAINT_MARK)), + sinkRule(SAMPLE_CLASS, "sinkSecond", SECOND_RULE_ID, listOf(Argument(0) to TAINT_MARK)), + ), + ) + + val traces = runAnalysis( + config = config, + entryPointClass = GeneratedSpringControllerDispatcher, + entryPointMethod = GeneratedSpringControllerDispatcherDispatchMethod, + apMode = ApMode.BaseOnly, + ) + + assertEquals(setOf(FIRST_RULE_ID, SECOND_RULE_ID), traces.mapTo(hashSetOf()) { it.vulnerability.rule.id }) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringRepositoryStaticFlowRegressionTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringRepositoryStaticFlowRegressionTest.kt new file mode 100644 index 000000000..063796f05 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/SpringRepositoryStaticFlowRegressionTest.kt @@ -0,0 +1,127 @@ +package org.opentaint.jvm.sast.dataflow + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.common.sast.dataflow.TaintAnalyzer +import org.opentaint.common.sast.dataflow.TaintAnalyzerOptions +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.MethodWithContext +import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.ifds.SingletonUnit +import org.opentaint.dataflow.ifds.UnitType +import org.opentaint.dataflow.ifds.UnknownUnit +import org.opentaint.dataflow.jvm.ap.ifds.JIRSafeApplicationGraph +import org.opentaint.dataflow.jvm.ap.ifds.analysis.JIRAnalysisManager +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.dataflow.jvm.ifds.JIRUnitResolver +import org.opentaint.ir.api.jvm.JIRMethod +import org.opentaint.ir.api.jvm.RegisteredLocation +import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.impl.features.usagesExt +import org.opentaint.jvm.graph.JApplicationGraphImpl +import org.opentaint.jvm.sast.dataflow.rules.TaintConfiguration +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcher +import org.opentaint.jvm.sast.project.spring.GeneratedSpringControllerDispatcherDispatchMethod +import org.opentaint.jvm.sast.project.spring.SpringRuleProvider +import org.opentaint.util.analysis.ApplicationGraph +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SpringRepositoryStaticFlowRegressionTest : AnalysisTest() { + companion object { + private const val SAMPLE_CLASS = "test.samples.SpringRepositoryStaticFlowSample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "spring-repository-static-flow" + } + + override val sourceFileExtension: String = "java" + + override fun customizeRulesProvider(rulesProvider: TaintRulesProvider): TaintRulesProvider = + SpringRuleProvider(rulesProvider, checkNotNull(context.springWebProjectContext)) + + @Test + fun `repository state saved by one controller action reaches another action with BaseOnly`() { + val config = SerializedTaintConfig( + entryPoint = listOf(entryPointRule(SAMPLE_CLASS, "update", TAINT_MARK, argIndex = 0)), + sink = listOf(sinkRule(SAMPLE_CLASS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))), + ) + + val treeRules = analyzeForward(config, ApMode.Tree) + val baseOnlyRules = analyzeForward(config, ApMode.BaseOnlyField) + + assertEquals(setOf(RULE_ID), treeRules) + assertEquals(setOf(RULE_ID), baseOnlyRules) + } + + private fun analyzeForward(config: SerializedTaintConfig, mode: ApMode): Set { + val noUnroll = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = false + } + val dispatcher = checkNotNull(cp.findClassOrNull(GeneratedSpringControllerDispatcher)) + .declaredMethods.single { it.name == GeneratedSpringControllerDispatcherDispatchMethod } + + val taintConfig = TaintConfiguration(cp).also { it.loadConfig(config) } + var rulesProvider: TaintRulesProvider = JIRTaintRulesProvider(taintConfig) + rulesProvider = JIRMethodExitRuleProvider(rulesProvider) + rulesProvider = customizeRulesProvider(rulesProvider) + + val usages = runBlocking { cp.usagesExt() } + val graph = JIRSafeApplicationGraph( + JTryBoundaryExceptionsApplicationGraph(JApplicationGraphImpl(cp, usages)), + ) + val projectLocation = dispatcher.enclosingClass.declaration.location + val unitResolver = object : JIRUnitResolver { + override fun resolve(method: JIRMethod): UnitType = + if (method.enclosingClass.declaration.location == projectLocation || + DataFlowApproximationLoader.isApproximation(method) + ) SingletonUnit else UnknownUnit + + override fun locationIsUnknown(loc: RegisteredLocation): Boolean = loc != projectLocation + } + + val analysisManagerHolder = arrayOfNulls(1) + val analyzer = object : TaintAnalyzer( + TaintAnalyzerOptions(ifdsTimeout = 1.minutes, ifdsApMode = mode), + ) { + override val unrollStrategy = noUnroll + override fun analysisGraph(): ApplicationGraph = graph + override fun analysisManager(): JIRAnalysisManager = JIRAnalysisManager( + cp, + refManager, + rulesProvider, + ).also { analysisManagerHolder[0] = it } + override fun unitResolver(): JIRUnitResolver = unitResolver + } + + return analyzer.use { + val engine = it.ifdsEngine + val analysisManager = checkNotNull(analysisManagerHolder[0]) + val startMethods = listOf(MethodWithContext(dispatcher, EmptyMethodContext)) + + analysisManager.selectPhase(TaintAnalysisManager.Phase.Prescan) + engine.resetApManager(TreeApManager(noUnroll, it.refManager, it.cancellation)) + engine.runAnalysis(startMethods, timeout = 1.minutes, cancellationTimeout = 30.seconds) + + analysisManager.selectPhase(TaintAnalysisManager.Phase.ShallowScan) + engine.resetApManager( + when (mode) { + ApMode.Tree -> TreeApManager(noUnroll, it.refManager, it.cancellation) + ApMode.BaseOnlyField -> BaseOnlyApManager(noUnroll, it.cancellation, fieldSensitive = true) + else -> error("Unsupported test mode: $mode") + }, + ) + engine.runAnalysis(startMethods, timeout = 1.minutes, cancellationTimeout = 30.seconds) + engine.getVulnerabilities().mapTo(hashSetOf()) { vulnerability -> vulnerability.ruleId } + } + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ThingsBoardEntityActionExplosionTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ThingsBoardEntityActionExplosionTest.kt new file mode 100644 index 000000000..9052d8bbb --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/ThingsBoardEntityActionExplosionTest.kt @@ -0,0 +1,215 @@ +package org.opentaint.jvm.sast.dataflow + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.opentaint.common.sast.dataflow.TaintAnalyzer +import org.opentaint.common.sast.dataflow.TaintAnalyzerOptions +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.MethodStats +import org.opentaint.dataflow.ap.ifds.MethodWithContext +import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy.AnyAccessorDisabled +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.ClassStatic +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.jvm.ap.ifds.JIRSafeApplicationGraph +import org.opentaint.dataflow.jvm.ap.ifds.analysis.JIRAnalysisManager +import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider +import org.opentaint.ir.api.jvm.JIRMethod +import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.impl.features.usagesExt +import org.opentaint.jvm.graph.JApplicationGraphImpl +import org.opentaint.jvm.sast.dataflow.rules.TaintConfiguration +import org.opentaint.util.analysis.ApplicationGraph +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +class ThingsBoardEntityActionExplosionTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + private val testClass = "test.samples.ThingsBoardEntityActionExplosionSample" + private val ruleId = "thingsboard-entity-action-explosion" + private val mark = "thingsboard-entity-action-taint" + private val config = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf(sinkRule(testClass, "sink", ruleId, listOf(Argument(0) to mark))), + ) + + private val safeContextRuleId = "thingsboard-context-support-safe" + private val taintedContextRuleId = "thingsboard-context-support-tainted" + private val contextSupportConfig = SerializedTaintConfig( + source = listOf(sourceRule(testClass, "source", mark)), + sink = listOf( + sinkRule(testClass, "safeContextSink", safeContextRuleId, listOf(Argument(0) to mark)), + sinkRule(testClass, "taintedContextSink", taintedContextRuleId, listOf(Argument(0) to mark)), + ), + ) + + private val classStaticRuleId = "thingsboard-class-static-context" + private val classStaticState = ClassStatic("thingsboard.class-static-context") + private val classStaticConfig = SerializedTaintConfig( + source = listOf( + sourceRule(testClass, "source", mark), + SerializedRule.Source( + function = functionMatcher(testClass, "seedClassStatic"), + condition = listOf(Argument(0) to mark).condition(), + taint = listOf( + SerializedTaintAssignAction( + kind = "ready", + pos = PositionBaseWithModifiers.BaseOnly(classStaticState), + ) + ), + ), + ), + sink = listOf( + sinkRule(testClass, "classStaticSink", classStaticRuleId, listOf(classStaticState to "ready")), + ), + ) + + @Test + fun `interface contexts multiply the branch-heavy entity action analysis`() { + val single = measureShallowScan(config, "singleEntityAction", "pushEntityActionToRuleEngine") + val contextual = measureShallowScan(config, "entityActionExplosion", "pushEntityActionToRuleEngine") + + assertEquals(setOf(ruleId), single.ruleIds) + assertEquals(setOf(ruleId), contextual.ruleIds) + + // The default ContextIndependentFacts policy shares only Zero and ClassStatic flows, so an + // ordinary interface-typed argument fact is still analyzed once per concrete context. + assertTrue( + contextual.stats.steps >= single.stats.steps * 4, + "six concrete interface contexts must multiply the shallow scan: " + + "single=${single.stats}, contextual=${contextual.stats}", + ) + println("ThingsBoard entity-action shallow scan: single=${single.stats}, contextual=${contextual.stats}") + } + + @Test + fun `class-static fact propagation is shared across argument type contexts`() { + val single = measureShallowScan(classStaticConfig, "singleClassStaticContext", "classStaticHotMethod") + val contextual = measureShallowScan(classStaticConfig, "classStaticContextExplosion", "classStaticHotMethod") + + assertEquals(setOf(classStaticRuleId), single.ruleIds) + assertEquals(setOf(classStaticRuleId), contextual.ruleIds) + + assertTrue( + contextual.stats.steps < single.stats.steps * 2, + "six type contexts should share context-independent zero and ClassStatic analysis: " + + "single=${single.stats}, contextual=${contextual.stats}", + ) + println("ThingsBoard class-static shallow scan: single=${single.stats}, contextual=${contextual.stats}") + } + + @Test + fun `context support batching preserves exact method contexts`() { + for (mode in listOf(ApMode.Tree, ApMode.BaseOnlyField)) { + val ruleIds = runAnalysis( + config = contextSupportConfig, + entryPointClass = testClass, + entryPointMethod = "contextSupportedSideEffectBatch", + apMode = mode, + ).mapTo(mutableSetOf()) { it.vulnerability.rule.id } + + assertEquals( + setOf(taintedContextRuleId), + ruleIds, + "$mode must not attach the tainted local transfer to the unsupported SafeContext", + ) + } + } + + @Test + fun `identical fact propagation is multiplied by its exact context support`() { + val single = analyzeContextWorkload("singleContextSupportedSideEffect") + val contextual = analyzeContextWorkload("contextSupportedSideEffectBatch") + + assertTrue( + contextual.steps >= single.steps * 4, + "seven contexts carrying the same fact must expose duplicated local work: " + + "single=$single, contextual=$contextual", + ) + println("ThingsBoard exact-context support: single=$single, contextual=$contextual") + } + + private fun analyzeContextWorkload(entryPoint: String): MethodStats.Stats { + var processStats: MethodStats.Stats? = null + val ruleIds = runAnalysis( + config = contextSupportConfig, + entryPointClass = testClass, + entryPointMethod = entryPoint, + apMode = ApMode.BaseOnlyField, + ) { analyzer, _ -> + val processMethod = cp.findClassOrNull(testClass)!!.declaredMethods + .single { it.name == "processContext" } + processStats = analyzer.ifdsEngine.collectMethodStats().stats[processMethod] + }.mapTo(mutableSetOf()) { it.vulnerability.rule.id } + + assertEquals( + setOf(taintedContextRuleId), + ruleIds, + "$entryPoint must retain the exact context-to-sink association", + ) + return requireNotNull(processStats) + } + + private class ShallowScanMeasurement(val ruleIds: Set, val stats: MethodStats.Stats) + + private fun measureShallowScan( + config: SerializedTaintConfig, + entryPointMethod: String, + hotMethodName: String, + ): ShallowScanMeasurement { + val cls = checkNotNull(cp.findClassOrNull(testClass)) + val entryPoint = cls.declaredMethods.single { it.name == entryPointMethod } + val hotMethod = cls.declaredMethods.single { it.name == hotMethodName } + + val taintConfig = TaintConfiguration(cp).also { it.loadConfig(config) } + var rulesProvider: TaintRulesProvider = JIRTaintRulesProvider(taintConfig) + rulesProvider = JIRMethodExitRuleProvider(rulesProvider) + rulesProvider = customizeRulesProvider(rulesProvider) + + val usages = runBlocking { cp.usagesExt() } + val graph = JIRSafeApplicationGraph( + JTryBoundaryExceptionsApplicationGraph(JApplicationGraphImpl(cp, usages)), + ) + + val managerHolder = arrayOfNulls(1) + val analyzer = object : TaintAnalyzer( + TaintAnalyzerOptions(ifdsTimeout = 1.minutes, ifdsApMode = ApMode.BaseOnlyField), + ) { + override val unrollStrategy = AnyAccessorDisabled + override fun analysisGraph(): ApplicationGraph = graph + override fun analysisManager() = + JIRAnalysisManager(cp, refManager, rulesProvider).also { managerHolder[0] = it } + override fun unitResolver() = this@ThingsBoardEntityActionExplosionTest + .unitResolver(cls.declaration.location) + } + + return analyzer.use { + val engine = it.ifdsEngine + val manager = checkNotNull(managerHolder[0]) + val startMethods = listOf(MethodWithContext(entryPoint, EmptyMethodContext)) + + manager.selectPhase(TaintAnalysisManager.Phase.Prescan) + engine.resetApManager(TreeApManager(AnyAccessorDisabled, it.refManager, it.cancellation)) + engine.runAnalysis(startMethods, timeout = 1.minutes, cancellationTimeout = 30.seconds) + val afterPrescan = engine.collectMethodStats() + + manager.selectPhase(TaintAnalysisManager.Phase.ShallowScan) + engine.resetApManager(BaseOnlyApManager(AnyAccessorDisabled, it.cancellation, fieldSensitive = true)) + engine.runAnalysis(startMethods, timeout = 1.minutes, cancellationTimeout = 30.seconds) + + val shallowDelta = engine.collectMethodStats().subtract(afterPrescan) + val ruleIds = engine.getVulnerabilities().mapTo(hashSetOf()) { v -> v.ruleId } + ShallowScanMeasurement(ruleIds, checkNotNull(shallowDelta.stats[hotMethod])) + } + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/TracePremiseCartesianTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/TracePremiseCartesianTest.kt new file mode 100644 index 000000000..55934e04a --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/TracePremiseCartesianTest.kt @@ -0,0 +1,358 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.ApMode +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.SummaryTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.FullStart2FinalTrace +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEdge +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntry +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver.TraceEntryAction +import org.opentaint.dataflow.ap.ifds.trace.MethodTraceResolver +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.ap.ifds.trace.withMethodRunner +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.ifds.SingletonUnit +import org.opentaint.ir.api.common.cfg.CommonInst + +class TracePremiseCartesianTest : AnalysisTest() { + override val sourceFileExtension: String = "java" + + private val testClass = "test.samples.TracePremiseCartesianSample" + private val mark = "trace-premise-cartesian" + private val oneClauseConfig = SerializedTaintConfig( + entryPoint = (0..1).map { entryPointRule(testClass, "entryOne", mark, it) }, + sink = listOf(sinkRule(testClass, "sink", "trace-premise-cartesian", listOf(Argument(0) to mark))), + ) + private val twoClauseConfig = SerializedTaintConfig( + entryPoint = (0..3).map { entryPointRule(testClass, "entry", mark, it) }, + sink = listOf(sinkRule(testClass, "sink", "trace-premise-cartesian", listOf(Argument(0) to mark))), + ) + + private val threeClauseConfig = SerializedTaintConfig( + entryPoint = (0..5).map { entryPointRule(testClass, "entryThree", mark, it) }, + sink = listOf(sinkRule(testClass, "sink", "trace-premise-cartesian", listOf(Argument(0) to mark))), + ) + + @Test + fun `one requested final keeps all origins in one grouped caller summary`() { + val tree = resolveCallerSummaries( + mode = ApMode.Tree, + config = oneClauseConfig, + entryMethod = "entryOne", + callerMethod = "multipleOriginsOne", + calleeMethod = "consumeOne", + calleeArgumentCount = 1, + ) + val baseOnly = resolveCallerSummaries( + mode = ApMode.BaseOnlyField, + config = oneClauseConfig, + entryMethod = "entryOne", + callerMethod = "multipleOriginsOne", + calleeMethod = "consumeOne", + calleeArgumentCount = 1, + ) + + assertCartesianFormula( + tree, + "Tree", + requestedFinalCount = 1, + alternativesPerFinal = 2, + traceCount = 2, + ) + assertGroupedFormula(baseOnly, "BaseOnly", requestedFinalCount = 1, alternativesPerFinal = 4) + } + + @Test + fun `two requested finals keep alternatives in one grouped caller summary`() { + val tree = resolveCallerSummaries( + mode = ApMode.Tree, + config = twoClauseConfig, + entryMethod = "entry", + callerMethod = "multipleOrigins", + calleeMethod = "consume", + calleeArgumentCount = 2, + ) + val baseOnly = resolveCallerSummaries( + mode = ApMode.BaseOnlyField, + config = twoClauseConfig, + entryMethod = "entry", + callerMethod = "multipleOrigins", + calleeMethod = "consume", + calleeArgumentCount = 2, + ) + + assertCartesianFormula(tree, "Tree", alternativesPerFinal = 2, traceCount = 4) + assertGroupedFormula(baseOnly, "BaseOnly", alternativesPerFinal = 4) + + val baseOnlyAlternatives = baseOnly + .flatMap { it.final.edges } + .toSet() + .groupBy(TraceEdge::fact) + .values + for (alternatives in baseOnlyAlternatives) { + val byOriginBase = alternatives.groupBy { edge -> + (edge as TraceEdge.MethodTraceEdge).initialFact.base + } + assertEquals(2, byOriginBase.size) + assertTrue(byOriginBase.values.all { it.size == 2 }) + assertTrue(byOriginBase.values.all { sameOrigin -> + sameOrigin.count { edge -> + (edge as TraceEdge.MethodTraceEdge).initialFact.isAbstract() + } == 1 + }) + assertTrue(byOriginBase.values.all { sameOrigin -> + sameOrigin.count { edge -> + TaintMarkAccessor(mark) in + (edge as TraceEdge.MethodTraceEdge).initialFact.getAllAccessors() + } == 1 + }) + } + } + + @Test + fun `three MethodEntry clauses stay grouped instead of materializing a cubic product`() { + val tree = resolveCallerSummaries( + mode = ApMode.Tree, + config = threeClauseConfig, + entryMethod = "entryThree", + callerMethod = "multipleOriginsThree", + calleeMethod = "consumeThree", + calleeArgumentCount = 3, + ) + val baseOnly = resolveCallerSummaries( + mode = ApMode.BaseOnlyField, + config = threeClauseConfig, + entryMethod = "entryThree", + callerMethod = "multipleOriginsThree", + calleeMethod = "consumeThree", + calleeArgumentCount = 3, + ) + + assertCartesianFormula( + tree, + "Tree", + requestedFinalCount = 3, + alternativesPerFinal = 2, + traceCount = 8, + ) + assertGroupedFormula( + baseOnly, + "BaseOnly", + requestedFinalCount = 3, + alternativesPerFinal = 4, + ) + } + + @Test + fun `action limit fallback resolves every grouped cube through all trace APIs`() { + var fallbackVerified = false + resolveCallerSummaries( + mode = ApMode.BaseOnlyField, + config = twoClauseConfig, + entryMethod = "entry", + callerMethod = "multipleOrigins", + calleeMethod = "consume", + calleeArgumentCount = 2, + ) { defaultResolver, limitedResolver, summaries -> + val summary = summaries.single() + val cancellation = Cancellation() + + val expectedStarts = defaultResolver.resolveIntraProceduralStart2FinalTrace(summary, cancellation) + val fallbackStarts = limitedResolver.resolveIntraProceduralStart2FinalTrace(summary, cancellation) + assertEquals( + expectedStarts.mapTo(hashSetOf()) { it.startEntry }, + fallbackStarts.mapTo(hashSetOf()) { it.startEntry }, + ) + + val expectedFull = defaultResolver.resolveIntraProceduralFullStart2FinalTrace( + summary, + cancellation, + collapseUnchangedNodes = false, + ) + val fallbackFull = limitedResolver.resolveIntraProceduralFullStart2FinalTrace( + summary, + cancellation, + collapseUnchangedNodes = false, + ) + assertEquals( + fullTraceEvidence(expectedFull), + fullTraceEvidence(fallbackFull), + ) + + val groupedStart = expectedStarts.first() + val expectedFullFromStart = defaultResolver.resolveIntraProceduralFullStart2FinalTrace( + groupedStart, + cancellation, + collapseUnchangedNodes = false, + ) + val fallbackFullFromStart = limitedResolver.resolveIntraProceduralFullStart2FinalTrace( + groupedStart, + cancellation, + collapseUnchangedNodes = false, + ) + assertEquals( + fullTraceEvidence(expectedFullFromStart), + fullTraceEvidence(fallbackFullFromStart), + ) + fallbackVerified = true + } + assertTrue(fallbackVerified) + } + + private fun resolveCallerSummaries( + mode: ApMode, + config: SerializedTaintConfig, + entryMethod: String, + callerMethod: String, + calleeMethod: String, + calleeArgumentCount: Int, + inspect: ((MethodTraceResolver, MethodTraceResolver, List) -> Unit)? = null, + ): List { + var result = emptyList() + val vulnerabilities = runAnalysis( + config = config, + entryPointClass = testClass, + entryPointMethod = entryMethod, + apMode = mode, + ) { analyzer, graph -> + val cls = cp.findClassOrNull(testClass) ?: error("Missing $testClass") + val caller = cls.declaredMethods.single { it.name == callerMethod } + val callee = cls.declaredMethods.single { it.name == calleeMethod } + val callerEntry = MethodEntryPoint( + EmptyMethodContext, + graph.methodGraph(caller).entryPoints().single(), + ) + val calleeEntry = MethodEntryPoint( + EmptyMethodContext, + graph.methodGraph(callee).entryPoints().single(), + ) + val call = caller.flowGraph().instructions.single { + it.toString().contains(calleeMethod) + } + val summaries = analyzer.ifdsEngine.getOrCreateUnitStorage(SingletonUnit) + ?: error("No summary storage") + val calleeInitials = (0 until calleeArgumentCount).map { argument -> + summaries.methodFactToFactSummaryEdges(calleeEntry, AccessPathBase.Argument(argument)) + .map { it.initialFactAp } + .single { initial -> + initial.base == AccessPathBase.Argument(argument) && + initial.getAllAccessors().contains(TaintMarkAccessor(mark)) + } + }.toSet() + + analyzer.ifdsEngine.withMethodRunner(callerEntry) { + val defaultResolver = methodTraceResolver(callerEntry) + result = defaultResolver.resolveIntraProceduralTraceFromCall( + call, + TraceEntry.MethodEntry(calleeInitials, calleeEntry), + ) + inspect?.invoke( + defaultResolver, + methodTraceResolver(callerEntry, traceResolutionActionHardLimit = 0), + result, + ) + } + } + assertTrue(vulnerabilities.isNotEmpty(), "$mode must preserve the source-to-sink flow") + return result + } + + private fun assertGroupedFormula( + traces: List, + mode: String, + requestedFinalCount: Int = 2, + alternativesPerFinal: Int, + ) { + assertEquals(1, traces.size, "$mode must keep the premise formula grouped") + assertEquals( + requestedFinalCount * alternativesPerFinal, + traces.single().final.edges.size, + "$mode grouped formula must retain every alternative", + ) + + val alternativesByFinal = traces + .flatMap { it.final.edges } + .groupBy(TraceEdge::fact) + .mapValues { (_, edges) -> edges.toSet() } + assertEquals(requestedFinalCount, alternativesByFinal.size) + assertTrue(alternativesByFinal.values.all { it.size == alternativesPerFinal }) + } + + private fun assertCartesianFormula( + traces: List, + mode: String, + requestedFinalCount: Int = 2, + alternativesPerFinal: Int, + traceCount: Int, + ) { + assertEquals(traceCount, traces.size, "$mode Cartesian trace count") + assertTrue(traces.all { it.final.edges.size == requestedFinalCount }) + + val alternativesByFinal = traces + .flatMap { it.final.edges } + .groupBy(TraceEdge::fact) + .mapValues { (_, edges) -> edges.toSet() } + assertEquals(requestedFinalCount, alternativesByFinal.size) + assertTrue(alternativesByFinal.values.all { it.size == alternativesPerFinal }) + assertEquals(traceCount, traces.mapTo(hashSetOf()) { it.final.edges }.size) + } + + private data class RuleActionEvidence( + val rule: CommonTaintConfigurationItem, + val actions: Set, + ) + + private data class FullTraceEvidence( + val starts: Set, + val actionStatements: Set, + val ruleActions: Set, + ) + + private fun fullTraceEvidence(traces: List): FullTraceEvidence { + val actionStatements = linkedSetOf() + val ruleActions = linkedSetOf() + + fun collectAction(action: TraceEntryAction?) { + when (action) { + is TraceEntryAction.CallRuleAction -> { + ruleActions += RuleActionEvidence(action.rule, action.action) + } + + is TraceEntryAction.SequentialSourceRule -> { + ruleActions += RuleActionEvidence(action.rule, action.action) + } + + else -> Unit + } + } + + for (trace in traces) { + val start = trace.startEntry as? TraceEntry.SourceStartEntry + collectAction(start?.sourcePrimaryAction) + start?.sourceOtherActions?.forEach(::collectAction) + + for (entry in trace.actionVariants.int2ObjectEntrySet()) { + actionStatements += trace.entries[entry.intKey].statement + for (variant in entry.value) { + collectAction(variant.primaryAction) + variant.otherActions.forEach(::collectAction) + } + } + } + + return FullTraceEvidence( + starts = traces.mapTo(linkedSetOf()) { it.startEntry }, + actionStatements = actionStatements, + ruleActions = ruleActions, + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/TryBoundaryExceptionsApplicationGraphTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/TryBoundaryExceptionsApplicationGraphTest.kt new file mode 100644 index 000000000..b6e5d008a --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/TryBoundaryExceptionsApplicationGraphTest.kt @@ -0,0 +1,46 @@ +package org.opentaint.jvm.sast.dataflow + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.opentaint.ir.api.jvm.cfg.JIRCatchInst +import org.opentaint.ir.api.jvm.cfg.JIRThrowInst +import org.opentaint.ir.api.jvm.ext.cfg.callExpr +import org.opentaint.ir.impl.features.usagesExt +import org.opentaint.jvm.graph.JApplicationGraphImpl +import org.opentaint.jvm.sast.ast.BasicTestUtils + +class TryBoundaryExceptionsApplicationGraphTest : BasicTestUtils() { + override val sourceFileExtension: String = "java" + + @Test + fun `explicit throws and the last try statement are connected to catch handlers`() { + val method = findMethod( + "test.samples.ExplicitExceptionEdgesSample", + "caughtExplicitThrow", + ) + val catch = method.instList.filterIsInstance().single() + val explicitThrow = method.instList.filterIsInstance().single() + val implicitThrowingCall = method.instList.single { + it.callExpr?.method?.method?.name == "implicitThrower" + } + val lastTryStatement = method.instList.single { + it.callExpr?.method?.method?.name == "lastTryStatement" + } + + val usages = runBlocking { cp.usagesExt() } + val baseGraph = JApplicationGraphImpl(cp, usages) + val graph = JTryBoundaryExceptionsApplicationGraph(baseGraph).methodGraph(method) + val selectedExceptionSources = graph.predecessors(catch).toSet() + + assertTrue(catch in graph.successors(explicitThrow).toSet()) + assertTrue(explicitThrow in graph.predecessors(catch).toSet()) + assertTrue(catch in graph.successors(lastTryStatement).toSet()) + assertTrue(lastTryStatement in graph.predecessors(catch).toSet()) + assertFalse(catch in graph.successors(implicitThrowingCall).toSet()) + assertFalse(implicitThrowingCall in graph.predecessors(catch).toSet()) + assertEquals(setOf(explicitThrow, lastTryStatement), selectedExceptionSources) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/AbstractSarifGeneratorTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/AbstractSarifGeneratorTest.kt index 9a66f90a6..9c7a81bd2 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/AbstractSarifGeneratorTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/AbstractSarifGeneratorTest.kt @@ -2,6 +2,7 @@ package org.opentaint.jvm.sast.sarif import io.github.detekt.sarif4k.Location import io.github.detekt.sarif4k.Region +import io.github.detekt.sarif4k.Result import io.github.detekt.sarif4k.ThreadFlowLocation import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.TestInstance @@ -20,10 +21,12 @@ abstract class AbstractSarifGeneratorTest: AnalysisTest() { val threadFlowLocations: List ) - fun generateSarifReport(traces: List): SarifData { + fun generateSarifResults( + traces: List, + options: SarifGenerationOptions = SarifGenerationOptions(), + ): List { val locs = cp.registeredLocations.filter { !it.isRuntime } val sourceFileResolver = JIRSourceFileResolver(sourcesDir, locs.associateWith { sourcesDir }) - val options = SarifGenerationOptions() val generator = JirSarifGenerator( options = options, @@ -32,9 +35,11 @@ abstract class AbstractSarifGeneratorTest: AnalysisTest() { traits = traits ) - val sarif = generator.generateSarif(traces.asSequence(), emptyList()) + return generator.generateSarif(traces.asSequence(), emptyList()).results.toList() + } - val results = sarif.results.toList() + fun generateSarifReport(traces: List): SarifData { + val results = generateSarifResults(traces) val resultLocations = results.flatMap { it.locations.orEmpty() } val threadFlowLocations = results .flatMap { it.codeFlows.orEmpty() } diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/JavaSarifGeneratorTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/JavaSarifGeneratorTest.kt index 49c3953a5..d0d73ee45 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/JavaSarifGeneratorTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/sarif/JavaSarifGeneratorTest.kt @@ -2,8 +2,10 @@ package org.opentaint.jvm.sast.sarif import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.opentaint.common.sast.sarif.SarifGenerationOptions import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import kotlin.test.assertEquals @TestInstance(TestInstance.Lifecycle.PER_CLASS) class JavaSarifGeneratorTest: AbstractSarifGeneratorTest() { @@ -13,6 +15,32 @@ class JavaSarifGeneratorTest: AbstractSarifGeneratorTest() { override val sourceFileExtension: String = "java" + @Test + fun `source sink fingerprint distinguishes rules sharing a sink`() { + val testCls = "$SAMPLE_PACKAGE.StaticFieldSample" + val config = SerializedTaintConfig( + source = listOf(sourceRule(testCls, "source", "tainted")), + sink = listOf( + sinkRule(testCls, "sink", "sink-rule-one", listOf(Argument(0) to "tainted")), + sinkRule(testCls, "sink", "sink-rule-two", listOf(Argument(0) to "tainted")), + ), + ) + + val traces = runAnalysis(config, testCls, "staticFieldFlow") + val results = generateSarifResults( + traces, + SarifGenerationOptions(generateFingerprint = true), + ) + + assertEquals(setOf("sink-rule-one", "sink-rule-two"), results.map { it.ruleID }.toSet()) + assertEquals( + 2, + results.map { + requireNotNull(requireNotNull(it.partialFingerprints)["vulnerabilitySourceSinkHash/v1"]) + }.toSet().size, + ) + } + @Test fun `flow with object constructor`() { val testCls = "$SAMPLE_PACKAGE.ConstructorFlowSample" diff --git a/docs/baseonly-access-domain-spec.md b/docs/baseonly-access-domain-spec.md new file mode 100644 index 000000000..20be34963 --- /dev/null +++ b/docs/baseonly-access-domain-spec.md @@ -0,0 +1,732 @@ +# BaseOnly access-domain specification + +Status: **normative** for the BaseOnly release mitigation. + +This document defines the BaseOnly access domain independently of its packed +representation. Production code, tests, serialization, and summary storage must +implement this document. Existing BaseOnly behavior and golden files are not +authoritative when they disagree with it. + +The Tree domain is the behavioral reference for the public `FactAp` interfaces. +The precise conformance obligations are in +[`baseonly-tree-conformance.md`](baseonly-tree-conformance.md). + +## 1. Goal and soundness boundary + +BaseOnly is a finite abstraction of Tree access paths and access trees. It may +merge Tree states and therefore report more flows, but it must not lose a Tree +flow merely because the packed form cannot retain Tree's precision. + +Let `Paths(X)` be the set of logical accessor paths accepted by a Tree or +BaseOnly value `X`. Let `project(X)` return a finite set of canonical BaseOnly +values (normally one; more are allowed when a Tree contains incompatible +branches). The fundamental invariant is: + +```text +Paths(X) ⊆ ⋃ { Paths(A) | A ∈ project(X) } +``` + +For every public operation `op`, every Tree result must be represented by a +BaseOnly result: + +```text +⋃ Paths(opTree(X, ...)) ⊆ ⋃ Paths(opBaseOnly(project(X), ...)) +``` + +Here an absent result, `null`, an empty result list, or a rejected summary has an +empty path set. Consequently, inability to represent a precise Tree result +requires widening; it never permits rejection. Base mismatch, a Tree-equivalent +exclusion, and a Tree-equivalent type incompatibility remain valid reasons to +reject. + +`project` is manager-relative because field-sensitive and field-insensitive +managers have different canonical states. + +## 2. Accessor alphabet and valid logical paths + +The accessor alphabet is partitioned as follows: + +| Category | Members | Symbol | +|---|---|---| +| static | `ClassStaticAccessor` | `S` | +| structural | `FieldAccessor`, `ElementAccessor` | `H` | +| implicit structural loop | `AnyAccessor` | `?` | +| taint semantic | `TaintMarkAccessor` | `M` | +| value semantic | `ValueAccessor` | `V` | +| type semantic | `TypeInfoAccessor`, optionally preceded by `TypeInfoGroupAccessor` | `T`, `G T` | +| terminal | `FinalAccessor` | `$` | + +A well-formed concrete path has this grammar: + +```text +path ::= static? structural* terminal +static ::= S +structural ::= H +terminal ::= $ + | M $ + | V M $ + | T $ + | G T $ +``` + +`AnyAccessor` is a Tree graph edge, not a concrete accessor in a path. BaseOnly +never stores it. A missing structural slot before a semantic or suffix-abstract +terminal implicitly denotes its universal structural self-loop. This deliberately +overapproximates a Tree `AnyAccessor` whose configured unroll strategy is narrower. +`T $` is the residual after consuming `G` from `G T $` and is also a +valid direct semantic path. `TypeInfoGroupAccessor` is a real logical step even +when BaseOnly stores the following type as one compact terminal component. + +Malformed orderings are rejected at public construction boundaries. A +projection of an otherwise valid Tree graph that cannot be expressed exactly is +widened at the earliest lost position. + +## 3. Canonical BaseOnly state + +A canonical access is the logical tuple: + +```text +(static, structural, terminal, valueAccessorState, abstraction) +``` + +where: + +- `static` is absent or one concrete `S`; +- `structural` is absent or the **outermost** concrete `H` after `static`; +- `terminal` is absent, `$`, a concrete taint mark `M`, or a concrete type `T`; +- `valueAccessorState` is `Normal` or `Value`. `Value` means that a taint-mark + suffix is preceded by `ValueAccessor`. For a type suffix, the same encoded state + reconstructs its analogous `TypeInfoGroupAccessor` prefix. Every + non-semantic state uses `Normal`; +- `abstraction` is absent or an abstract node at exactly one of the three + positions `STATIC`, `STRUCTURAL`, or `SUFFIX`. + +The suffix atom stores the concrete semantic accessor and the state records +whether its category wrapper occurs immediately before it. Let `W(M) = V` and +`W(T) = G`. For a semantic accessor `X`, the denotation is: + +```text +TerminalPath(X, Normal) = X $ +TerminalPath(X, Value) = W(X) X $ +``` + +One packed access denotes one terminal path. A union containing both paths is +represented by two facts and is never encoded as a third state. This distinction +prevents `M $` from being confused with `V M $`, and a type residual `T $` from +being confused with `G T $`. + +The implicit `$` belongs to every alternative. Public reads, accessor views, +clear, exclusions, filtering, relations, residuals, concat, storage, +serialization, and rendering operate on this logical alternative set. No +operation may recreate a wrapper state that was removed, except by retaining a +separate fact carrying that state. + +### 3.1 Retention rule + +Construction and composition always retain: + +1. the first (outermost) static accessor; +2. in field-sensitive mode, the first (outermost) concrete structural accessor; + in field-insensitive mode, no structural field slot; +3. the first well-formed semantic terminal and whether its parsed path is direct + or wrapper-prefixed. + +A second distinct static is invalid. Later structural accessors are not allowed +to replace the retained outermost accessor. In field-insensitive mode no +concrete structural identity is retained. An explicit Tree `Any` is projected +to the same absent structural slot in either mode. Every semantic root has the +implicit structural self-loop. + +When incompatible projected branches differ in an ordinary retained component, +`canonicalJoin` retains their common canonical prefix and places abstraction at +the first position where they differ. When they differ only in +`valueAccessorState`, it returns both facts. Collection interfaces keep this +minimal covering set and never manufacture a union state. + +When structural information is discarded: + +- a discarded or explicit structural branch is represented by an absent field + slot and the implicit structural loop; +- `V M $` projects to `(M, Value)` and `G T $` projects to `(T, Value)`; + the wrapper is not added to a direct terminal; +- an abstract suffix state already widens by its implicit structural-`Any` + transition; +- an exact `$` terminal cannot express a discarded structural step and therefore + widens to an abstract suffix at the last exact prefix; +- if the lost step precedes the retained structural accessor or static accessor, + widening moves to the corresponding earlier abstraction position. + +This rule applies identically to `build`, `prepend`, `append`, final concat, +initial concat, Tree projection, deserialization, and summary normalization. + +### 3.2 Abstract positions + +An abstract marker represents Tree's abstract-node acceptance at one category +boundary: + +- `STATIC`: no prefix is committed; +- `STRUCTURAL`: the optional concrete static prefix is committed; +- `SUFFIX`: the optional concrete static and structural prefix is committed. + +`SUFFIX` and semantic terminal states have an implicit structural-`Any` +self-loop. `AnyAccessor` is never an explicit stored graph edge. Earlier abstract +positions are refinement boundaries and do not fabricate a public outgoing +edge. + +An abstraction marker terminates the canonical tuple. Components after it must +be absent. There is at most one abstraction marker. + +### 3.3 Canonical validity + +The following are valid: + +```text +STATIC abstract: (*, -, -, STATIC) +STRUCTURAL abstract: (S?, *, -, STRUCTURAL) +SUFFIX abstract: (S?, H?, -, SUFFIX) +exact value: (S?, H?, $, none) +semantic value: (S?, H?, X, Normal | Value, none) +``` + +Here `X` is a concrete taint mark or type. `Normal` and `Value` denote +`TerminalPath(X, state)` above. Only semantic values may use `Value`; every +abstract, empty, exact-`$`, and transient state uses `Normal`. + +The internal empty access is valid only as an intermediate or empty delta. A +fact must never contain it. + +The following are invalid: + +- multiple abstraction markers; +- a component following an abstraction marker; +- a static accessor outside the static component; +- `AnyAccessor` in any packed slot (it is implicit and has no stored slot); +- `TypeInfoGroupAccessor` without a following type; +- `ValueAccessor` without a following taint mark; +- a `Value` state on a non-semantic suffix; +- a semantic terminal without its logical `$`; +- more than one semantic terminal; +- a nonempty exact prefix with neither a terminal nor an abstraction; +- an accessor index that does not belong to the slot category; +- an index outside the codec's documented range. + +`COLLAPSED_MARK` is not a stable domain state. It has no standalone +Tree denotation and is forbidden in deltas, storage, initial facts, and +serialization. One transient operational state is reserved for +flow-function recursion: +`COLLAPSED` in the suffix slot. It means that suffix abstraction was temporarily +removed while the concrete prefix is processed. It may exist only in a final +fact returned by `removeAbstraction`; it is restored to suffix abstraction by +`rebase`. Storage, initial facts, deltas, and serialization reject it. + +### 3.4 Packed codec + +The current packed `Long` reserves 16 bits for the static component, 24 bits for +the structural component, and 24 bits for the suffix word. The suffix word is +split into a **23-bit biased accessor value** and a one-bit value-accessor-state +flag. With bias `3`, the suffix accessor range is `-3..8_388_604` inclusive. +`Normal` and `Value` use flag values `0` and `1`. Static and structural retain +their existing 16-bit and 24-bit +biased ranges. These widths are implementation limits, not domain semantics. + +Reducing the suffix accessor payload from 24 to 23 bits is a format invariant: +construction, raw packing, deserialization, and interner-to-slot conversion must +reject an out-of-range suffix rather than truncate its high bits into the +state flag. The state bit is zero for every non-semantic suffix. + +Raw packing and unpacking are codec-internal. The codec must validate category, +range, uniqueness, ordering, and canonical form. Deserialization is: + +```text +decode -> validate -> canonicalize -> construct +``` + +No public or storage API may accept an arbitrary packed `Long` as a valid fact. + +## 4. Logical graph and accessor views + +Every operation in this section is derived from one logical graph view. + +Concrete static components form ordinary single edges. A retained concrete +structural component denotes that edge followed by zero or more projected-away +structural edges before the terminal; after consuming it, the implicit +`AnyAccessor` loop is exposed when a semantic terminal remains. A missing +structural slot before a semantic or suffix-abstract state forms the same loop +and admits its suffix alternatives at zero length. A semantic terminal expands to exactly the +single path selected by `valueAccessorState`; it does not gain the other path +implicitly. A `SUFFIX` abstract state exposes the +implicit `AnyAccessor` self-loop required by its widening. + +### 4.1 `consume`, `readAccessor`, and `startsWithAccessor` + +`consume(A, a)` follows the logical outgoing edge `a` and returns the canonical +residual state. It returns no result if no such edge exists. + +```text +startsWithAccessor(a) == (consume(A, a) exists) +readAccessor(a) == consume(A, a), wrapped with the same base/exclusions +``` + +Reading a concrete prefix removes that prefix and may expose the implicit +structural residual. Reading a concrete structural accessor through the implicit +Any self-loop of a semantic or suffix-abstract state returns the same state. +Reading a semantic accessor advances each matching +logical alternative: + +- `(X, Normal)` reads `X` to `$` and does not read `W(X)`; +- `(X, Value)` reads `W(X)` to `(X, Normal)` and does not read `X` at the + wrapper root; + +The residual after a wrapper read is always `Normal`; the wrapper has already +been consumed. The same rules apply when the terminal is admitted at zero length +through implicit Any. + +`AnyAccessor` is never stored. A missing structural slot before a semantic or +suffix-abstract terminal denotes the implicit Any self-loop and accepts every +concrete structural read without enumerating fields. + +### 4.2 `getStartAccessors` + +This returns the set of logical outgoing edge labels at the current node. It +includes `AnyAccessor` whenever the compact state has the implicit structural +self-loop. It does not expand that loop into concrete fields. + +For a root semantic `(X, state)`, the start set is: + +```text +Normal -> { AnyAccessor, X } +Value -> { AnyAccessor, W(X) } +``` + +Thus a compact semantic root exposes both its terminal alternative and its +implicit structural loop. A suffix-abstract root returns `{AnyAccessor}`; its +suffix alternatives are +readable through the zero-length wildcard but are not additional raw root-edge +labels. + +### 4.3 `getAllAccessors` + +This returns all **concrete logical accessors** occurring anywhere in the +represented graph. As in Tree's `collectAccessorsTo`, it deliberately excludes +`AnyAccessor`, even though `getStartAccessors` exposes it. For a semantic +terminal it always includes `X` and `$`; it includes `W(X)` exactly when the +state is `Value`. It must not report `ValueAccessor` or +`TypeInfoGroupAccessor` when the state is `Normal`. + +The two accessor views are intentionally asymmetric and must not share a raw +slot iterator. + +### 4.4 Head, size, depth, and abstract status + +- `headOrNull` and `firstAccessorOrNull` follow the one path selected by the + access's value-accessor state. A set containing both alternatives is handled + by iterating its two facts; no individual access has two semantic heads. +- `size` counts occupied concrete packed slots. Static, field, and suffix each + contribute at most one; abstract markers, missing slots, implicit Any, and a + logical value/type wrapper do not contribute. Therefore `0 <= size <= 3`. +- `depth` equals this compact size. It is a bounded retention metric, not an + attempt to reproduce Tree's node count, logical wrapper depth, or Any-cycle + sentinel. +- `isAbstract` is true exactly when the current logical node has abstract + acceptance. An abstraction after a concrete prefix becomes current only after + that prefix is consumed. Delta emptiness is independent of abstractness. + +These metrics intentionally describe the compact representation. Semantic +operations must not use them as logical-path lengths. + +## 5. Canonical construction + +### 5.1 `canonicalize` and `build` + +`canonicalize(sequence, fieldSensitive)` parses a well-formed logical accessor +sequence, applies the retention/widening rule in section 3.1, validates the +result, and returns its unique canonical state. It is idempotent. + +`build(accessors, isAbstract)` is a compatibility entry point for +`canonicalize`. `isAbstract` adds abstract-node acceptance after the supplied +sequence; it does not silently reorder malformed input. A sequence ending in a +semantic accessor expands its implied `$` only when that convention is explicit +at the caller boundary; the canonical state always records the same terminal +meaning. + +Construction assigns the value-accessor state from the parsed sequence: + +```text +M [$] -> (M, Normal) V M [$] -> (M, Value) +T [$] -> (T, Normal) G T [$] -> (T, Value) +``` + +The wrapper must be followed by the corresponding semantic category. A lone +`V` or `G`, `G M`, `V T`, multiple semantics, or anything after `$` is invalid. +`build` returns one access and therefore one of the two states. + +### 5.2 `abstractAt` + +`abstractAt(prefix, position)` canonicalizes the exact prefix followed by an +abstract node at one of the validated `STATIC`, `STRUCTURAL`, or `SUFFIX` +positions (currently encoded as `0..2`). A prefix component at or after the +abstract position is invalid. + +### 5.3 `prependAccessor` + +`prepend(A, a)` is: + +```text +canonicalize([a] + logicalPaths(A)) +``` + +for every represented path, joined by the least canonical widening if required. +It obeys the outermost-retention rule in the **composed** path: a prepended +structural accessor becomes the new outermost structural accessor, while a +structural accessor appended at an abstraction cannot replace the already-known +outer prefix. Impossible Tree prepends remain impossible; representational loss widens. + +`TypeInfoGroupAccessor` is accepted only before an already compact type suffix +and changes that terminal to `Value`. `ValueAccessor` is accepted only before +an already compact taint-mark suffix and likewise changes it to `Value`. This +models prepending the wrapper to the unwrapped residual; it does not merge paths. +A standalone or category-mismatched wrapper is rejected. + +### 5.4 `graft`, append, and concat + +`graft(prefix, suffix, typeChecker?)` substitutes each abstract accepting leaf +of `prefix` with `suffix`, like Tree concat, and canonicalizes the union. + +- empty delta is the identity; +- a nonempty suffix with a static accessor can be grafted only at a position + where Tree allows that static accessor; +- incompatible paths are rejected only when Tree/type checking rejects them; +- discarded precision causes widening: when two structural steps compete for + the one retained field slot, keep the earlier step and preserve an incoming + semantic terminal behind its implicit structural-Any tail. The earlier step + includes the virtual Any represented by an absent field in a suffix-abstract + prefix; it consumes a concrete suffix field instead of allowing that field to + occupy the empty slot. If the suffix ends only in exact `$`, widen to suffix + abstraction because no terminal can represent the discarded step; +- initial-delta concat uses the same graft without a type checker; +- final-delta concat uses the supplied `FactTypeChecker` and must not recreate a + Tree-rejected or primitive-incompatible path. + +`append` and `appendFinal` are implementation wrappers around `graft`; they do +not have independent slot-case semantics. + +## 6. Relations + +Three different relations are required. + +### 6.1 Exact equality + +Canonical access equality means equal canonical logical paths, including equal +value-accessor state. `Normal` and `Value` are unequal. Fact equality +also requires equal base and exclusions. Initial/final cross-kind `equalTo` +compares their logical projected graphs under the Tree cross-kind definition; +it is not overlap. + +### 6.2 Directional coverage + +```text +covers(pattern, fact) iff Paths(fact) ⊆ Paths(pattern) +``` + +Coverage is reflexive and transitive. It is used by authoritative storage +subsumption and canonical joins. A missing structural slot before `M` includes +the implicit Any loop, so compact `M.$` covers both its zero-length path and +`f.M.$`. A suffix-abstract pattern likewise covers concrete structural +continuations through its implicit Any loop. + +For equal semantic accessor `X`, value-accessor states must be equal: + +```text +Normal covers Normal only +Value covers Value only +``` + +Different semantic accessors never cover one another. `containsProjected` uses +the same value-accessor-state direction after its separate structural-slot compatibility +check. + +Base and exclusions are not part of access-only coverage. Public final-to-initial +fact containment first requires base equality, then uses the +projection-aware `containsProjected` relation: corresponding concrete slots must +agree, abstraction covers descendants, and a missing structural slot is compatible +with a retained structural slot because either side may be the projection of the +same longer Tree path. This symmetric slot compatibility is intentionally broader +than `covers`; it is required by trace entry matching and is not storage subsumption. +Exclusions do not change that query. Public **initial +fact** `contains` projects Tree's exact `AccessPath.contains`: base remains +exact, while access equality widens to a zero-residual prefix match and +exclusions are ignored. These widenings are required because distinct Tree paths +and their path-local exclusion state may collapse to one canonical BaseOnly +access. Storage subsumption that needs broader directional coverage still calls +`covers` explicitly. + +### 6.3 Symmetric overlap + +```text +mayOverlap(a, b) iff Paths(a) ∩ Paths(b) ≠ ∅ +``` + +Overlap is reflexive and symmetric, but need not be transitive. It is used only +for candidate indexing and never as containment. Candidate indexes may return a +superset of overlapping values, provided an authoritative relation is applied +afterward. + +For equal semantic accessor `X`, two accesses overlap only when their +value-accessor states are equal. + +The test-reference `canonicalJoin` returns the minimal fact set covering both denotations. If its +operands have the same prefix and semantic accessor but different +value-accessor states, the result contains both operands. A join must not +discard either path or change its wrapper state. If prefixes or semantic +accessors differ, the ordinary earliest-difference abstraction rule applies and +may produce one widened access. + +The symmetric “missing field is compatible” predicate implements only +`containsProjected`; it must not implement `covers`. + +## 7. Residuals, delta, and split-delta + +`residual(pattern, fact)` returns the canonical suffix deltas needed to +reconstruct the paths of `fact` matched by `pattern`. It is defined by logical +graph quotient, not AP-slot cases. + +For every returned delta `D`: + +```text +Paths(fact matched by pattern) ⊆ Paths(graft(pattern, D)) +``` + +The result is empty exactly when there is no match. It contains the empty delta +when the match includes identity. It may contain both empty and nonempty deltas, +as Tree final delta does for a node having abstract acceptance plus concrete +children. + +Residuals preserve the value-accessor state of every unmatched terminal path. A +wrapper residual is `(X, Value)` before its wrapper is consumed and +`(X, Normal)` after it is consumed. When a collection contains both paths, each +is processed independently. Residual computation must not merge them merely +because they share the same compact suffix accessor. + +### 7.1 Final `delta` + +`final.delta(initial)` first requires equal bases. It computes the quotient of +the final logical graph by the initial linear pattern, applies the initial +exclusions to the residual's logical first branches, and projects every +surviving Tree delta. + +If the residual starts at a compact semantic terminal, exclusions are applied +to that fact's root path before the delta is returned. Excluding `X` removes an +`Normal` fact; excluding `W(X)` removes a `Value` fact. A collection retains +the other fact independently. + +### 7.2 Initial `splitDelta` + +`initial.splitDelta(finalPattern)` first requires equal bases. It finds the +longest Tree-valid matched initial prefix and returns `(matchedInitial, delta)` +pairs whose concat covers the original initial. Exclusions on the final pattern +filter the first logical branches of the delta. No behavior may depend on a +hand-written pair of abstraction slots. + +Matched initial accesses and returned deltas retain value-accessor state. In +particular, a split of `W(X) X $` cannot return a direct-root delta until the +wrapper belongs to the matched prefix. + +### 7.3 Delta concat + +Initial delta concat is logical path concatenation followed by canonicalization. +Empty is a two-sided identity. Concat is associative after canonicalization. +Final deltas are grafted through final-fact concat. + +Composition copies the semantic terminal and value-accessor state from the operand that +contributes that terminal. Grafting or appending a wrapped suffix stays wrapped; +an unwrapped suffix stays unwrapped. When alternative operands with the same semantic +accessor are joined, concat delegates to `canonicalJoin`, which returns both +facts. Concat itself never changes `Normal` to `Value` merely because the +wrapper is representationally compact. + +## 8. Exclusions and clear + +An exclusion set filters outgoing **logical branches at the point where the +interface applies it**: + +- `Empty` allows all branches; +- `Concrete(E)` removes branches whose concrete logical edge is in `E`; +- excluding `TypeInfoGroupAccessor` excludes every type-info group branch; +- excluding a concrete `TypeInfoAccessor` excludes that type branch only; +- `Universe` removes all branches and is legal only at interfaces that explicitly + accept it; otherwise it is rejected as an invariant violation. + +At a root compact semantic terminal, an exclusion may remove the zero-length +terminal branch, but the same terminal remains reachable after the implicit Any +loop. Exact subtraction is not representable, so BaseOnly retains the compact +cover. All delta, split, abstraction, side-effect, and storage code follows this +same conservative rule. + +`clearAccessor(a)` subtracts every represented root branch labeled `a`. If exact +subtraction is representable, it is returned. If all paths are removed, it +returns `null`. If subtraction is not representable, the operation returns the +least canonical **overapproximation of the surviving paths**. Such a widening +may retain a cleared path as a false positive, but it must never remove an +unrelated surviving Tree path. Clearing `AnyAccessor` removes the Any branch +itself, not every concrete structural branch. + +For a root compact semantic terminal, clearing a terminal label leaves the +compact state unchanged: the zero-length branch is removed, while the same +terminal remains reachable after the implicit Any loop. This is the least +representable cover of the survivors. + +## 9. Type filtering + +`FactApFilter` traverses every edge of the logical graph, including implicit +group/type/final steps and the implicit Any loop. Compatibility filtering follows Tree's +different rule: it consults an accessor only when that edge's child has direct +abstract acceptance. Ancestor edges that merely lead eventually to an abstract +descendant, and wholly concrete paths, are retained without consulting the +compatibility checker. The filters are applied per fact as in Tree. If +BaseOnly merges accepted and rejected paths, it keeps a sound projection of the +accepted branches; it must not reject the whole fact merely because one +represented branch is rejected. + +For a semantic suffix, `FactApFilter` evaluates the one complete path selected +by its value-accessor state: `X $` for `Normal` or `W(X) X $` for `Value`. +It returns the same state when that path survives and `null` otherwise. A set +containing both paths invokes the filter independently for each fact. + +Tree's `FactCompatibilityFilter` is narrower than `FactApFilter`: it checks only +an edge whose surviving child is abstract, and removes that child's abstract +acceptance when the edge is incompatible. It never rejects an exact path merely +because one of its concrete accessors is incompatible. In a canonical BaseOnly +fact, only the last committed accessor immediately before the single abstract +position is therefore checked; a root abstraction has no such concrete edge. + +`FinalFactAp.concat(typeChecker, delta)` performs the same branch-wise check +during graft. `InitialFactAp.compatibilityFilter` is built from the logical +accessor sequence/graph, not the packed slots. + +## 10. Abstraction lifecycle and rebasing + +- `mostAbstractInitialAp(base)` is the projection of Tree's null initial access. +- `mostAbstractFinalAp(base)` is the projection of Tree's abstract root. +- `abstractOnly()` preserves an existing static- or field-position abstraction. + Other facts become suffix-position abstract. This is the restored historical + BaseOnly behavior; a single precise representation of Tree's abstract root is + still unspecified. +- `removeAbstraction()` suppresses the current abstract acceptance for the duration + of one flow-function step. A suffix-position abstraction, including the + most-abstract final fact, becomes the transient `COLLAPSED` state. Field-position + abstraction is projected to the later suffix abstraction when the compact + domain cannot express a terminating concrete prefix. Suffix abstraction after + a concrete prefix also becomes the transient `COLLAPSED` state. +- `rebase(newBase)` changes the base and completes that operational lifecycle by + restoring `COLLAPSED` to suffix abstraction. For stable facts it changes + only the base. +- `exclude` and `replaceExclusions` change only exclusions. + +Initial-fact abstraction constructs the same refinement ladder as Tree after +projection. It uses `FactTypeChecker` and `AnyAccessorUnrollStrategy`, emits no +mixed concrete-initial/abstract-final identity edge, and deduplicates canonical +logical pairs. Its behavior is specified by Tree projection rather than by a +fixed three-slot case table. + +## 11. Fact and manager factories + +Every factory validates canonical form. All BaseOnly values in one analysis are +assumed to use the same manager/interner. + +- `createFinalAp(base, exclusions)` creates the exact `$` final fact. +- `createFinalInitialAp(base, exclusions)` creates the exact `$` initial fact. +- facts cannot wrap the internal empty access; +- deltas may wrap empty only through the dedicated empty-delta singleton; +- equality and hashing use base, packed access, and exclusions only; manager + identity is intentionally absent under the single-manager invariant. + +## 12. Serialization and diagnostics + +The serialized payload encodes the logical state: + +- base and exclusions; +- optional static and structural accessor identities; +- terminal kind and logical semantic accessor identities; +- value-accessor state (`Normal` or `Value`); +- abstraction kind/position. + +It does not encode `size` followed by a differently-sized iterator. Every valid +canonical fact round-trips to exact canonical equality. Invalid, unknown, empty +fact, out-of-range, and noncanonical states fail predictably. + +The codec writes the three tagged logical slots followed by one value-accessor-state +byte. It has no BaseOnly magic, header, or version field. Accessor identities are +resolved through the serialization context and the current 23-bit suffix range +is enforced rather than truncated. + +Rendering is unambiguous and representation-independent. It distinguishes +abstract positions, implicit Any, concrete terminal kinds, value-accessor state, +and every retained prefix. `Normal` and `Value` must render distinctly. +Rendering and parsing are diagnostic only and are never used to infer semantics. + +## 13. Required shared primitives + +The target production architecture has exactly one implementation of each +decision below. The operation ledger identifies the decisions that have not yet +been consolidated; declaring the primitive here is a requirement, not evidence +that delegation is already complete. + +```text +canonicalize logical sequence/graph -> canonical access set +canonicalJoin two accesses -> minimal covering access set +logicalGraph canonical access -> logical graph/view +terminalPath semantic accessor x value-accessor state -> one path +consume access x accessor -> residual access? +covers directional language inclusion +containsProjected projected final-to-initial trace/fact compatibility +mayOverlap symmetric nonempty intersection +residual pattern x fact -> delta set +graft prefix x delta x optional type checker -> access set +exclusionAllows logical branch x exclusion -> boolean +removeBranches logical graph x predicate -> canonical graph set +``` + +Facts and deltas may wrap these results but must not reimplement their decisions. + +## 14. Algebraic laws + +All laws apply to valid canonical states from the analysis's single manager. + +1. `canonicalize(canonicalize(A)) == canonicalize(A)`. +2. Projection is sound and monotone under Tree graph inclusion. +3. Metamorphic construction routes (`build`, repeated prepend, graft) have the + same canonical projection when they describe the same logical graph. +4. `startsWith(A,a) == (consume(A,a) != null)`. +5. `readAccessor` is `consume` with base/exclusions preserved. +6. `getStartAccessors` is exactly the logical root-edge set, including Any. +7. `getAllAccessors` is the logical transitive concrete-accessor set, excluding + Any. +8. `covers` is reflexive and transitive. +9. `mayOverlap` is reflexive and symmetric. +10. Equality implies mutual coverage; overlap implies neither equality nor + coverage. +11. `residual(P,F)` is empty iff `P` cannot match `F`. +12. Every residual reconstructs a cover of its matched fact through `graft`. +13. Empty delta is a two-sided concat identity. +14. Delta concat is associative after canonicalization. +15. Prepend and consume form a left inverse whenever Tree prepend is exact. +16. Clear never removes an unrelated Tree survivor; when exact subtraction is + unrepresentable its result is the least canonical cover of the survivors. +17. Exclusion filtering is monotone: adding exclusions cannot add paths. +18. Type filtering never removes a Tree-compatible path. +19. Rebase changes only base. +20. Serialization round-trips every valid stable state and rejects the + transient collapsed state. +21. `Normal` and `Value` are distinct states. Joining them returns two facts; + neither state covers or overlaps the other. +22. Reading a wrapper from `Value` produces `Normal`; reading the semantic + accessor succeeds only from `Normal`. +23. Clear, exclusion, and filtering preserve a cover of every surviving + terminal alternative; implicit-Any subtraction may conservatively retain + the original compact state. +24. Residual and concat preserve value-accessor state; an explicit join retains + differing states as separate facts. + +Release verdict (1) requires a bounded exhaustive test and a Tree differential +counterpart for each applicable law. The operation ledger is authoritative for +current coverage; a behavior example is not a substitute for these laws. diff --git a/docs/baseonly-clean-branch-selection.md b/docs/baseonly-clean-branch-selection.md new file mode 100644 index 000000000..af595725b --- /dev/null +++ b/docs/baseonly-clean-branch-selection.md @@ -0,0 +1,168 @@ +# Ranking of non-BaseOnly changes by perf impact (conductor / thingsboard) + +Baseline = `feb2094fa` (tag `baseline-experiments`). Fork point = `6adc217f2`. +97 commits, 252 files, +35151/-847. Scope here excludes `ap/ifds/access/baseonly/**`. + +**Two of the changes ranked Tier 1 below were later removed** — see "Tier 6" and +`e2e-regression-2026-08-19.md`. This document was written before that end-to-end run and the +ranking is left as it was recorded, with the two entries moved out of Tier 1 rather than edited +in place, so the reasoning that promoted them stays visible next to the evidence that demoted +them. The measured-results tables below describe the branch *including* both; the branch no +longer contains either, and no benchmark difference was measurable without them. + +Measurement protocol: `-Xmx12g`, quiet machine, alternating configs, N reps, **min** per cell +(interference only adds time, so min is the robust estimator). + +--- + +## Tier 0 — Architecture. Not optional; removing any of it removes the experiment. + +| # | Change | Why it is Tier 0 | +|---|---|---| +| 0.1 | `Phase.ShallowScan` + `Phase.FullScan(actionableRules)` payload | the staged contract itself | +| 0.2 | `TaintAnalyzer.analyzeStaged` 3-phase driver + budget split | the pipeline | +| 0.3 | **`SelectedTaintRulesProvider` / `SelectedGoTaintRulesProvider`** — statement-exact rule gating | *the* full-scan cost reduction ("method-level rule search") | +| 0.4 | `TraceActionSearcher` + `resolveVulnerabilityActionableRules` | produces the rule map | +| 0.5 | `selectRules(relevantRuleIds)` moved Prescan→ShallowScan | shallow phase runs on the reduced rule set | +| 0.6 | **`overApproximateMethodContext`** selective context sharing | "better method context management for the shallow scan" | +| 0.7 | Phase-boundary correctness set: `analyzerEnqueued` reset; AP-reset rebuilds summary serializer; non-positive timeout guards; `Cancelled : CancellationException`; per-run `MemoryManager` state | without these the staged pipeline silently drops work or aborts the full scan on its first GC | +| 0.8 | F2F `add` returns a list of changed finals | BaseOnly layered summaries need it; also fixes real dropped-delta bugs in Cactus/Automata | + +## Tier 1 — Largest structural wins (keep) + +| # | Change | thingsboard | conductor | +|---|---|---|---| +| 1.3 | BaseOnly conclusion-grouped F2F worklist + `createFactToFactTransfer` | HIGH | MED | +| 1.4 | `TraceEdges` conjunctive premises + exact-cube fallback + exclusion normalization | MED | HIGH | +| 1.5 | Boundary quotient: `InterProceduralMethodEntryNode` + field-generalization start-trace memo | MED | HIGH | +| 1.6 | Over-approximate start-trace resolution | MED | HIGH | +| 1.7 | `JTryBoundaryExceptionsApplicationGraph` (also a correctness fix) | HIGH | LOW-MED | + +## Tier 2 — Real but smaller; cheap to keep + +Rule-storage lock removal; zero-to-zero edge prioritization; skip empty summary-delta +publications; BaseOnly wildcard call-summary domination; `entriesThatCanReach` linear +corridor reachability; ND summary dedup / sequent accumulation / ND activation batching; +side-effect requirement delta tracking; lazy condition rewriter; `mkFalse` singleton; +`trackExternalMethod` early exit; summary-rewriter identity fast path. + +## Tier 3 — Correctness fixes worth keeping regardless of perf + +`ConcurrentReadSafeObject2IntMap` seqlock; `JIRCallResolver` override-cache key +(`method` → `method+baseClass`); Spring overloaded-controller entry points; bridge-method +argument filtering. + +## Tier 6 — Removed after end-to-end measurement + +Both were ranked Tier 1 on benchmark time and both were deleted after the 2026-08-19 e2e run +across 28 projects showed them costing recall. Neither removal was measurable on thingsboard or +conductor, which is the whole point: the two benchmarks that promoted them could not see what +they cost. Full evidence in `e2e-regression-2026-08-19.md`. + +| # | Change | Why it was removed | +|---|---|---| +| 6.1 | shallow-scan statement collapsing: an analysis-manager predicate for "this statement is provably the identity on this fact", plus the analyzer's transparent-successor walk and its per-(statement, fact) closure memo | Forward-only, with no mirror in the backward resolver. The trace resolver probes for edges at statements the forward pass deliberately never wrote, so resolution fails and the finding is dropped: **11 lost findings**. The mirror was implementable, but with no measurable benefit to protect it there was nothing to weigh against the risk. | +| 6.2 | class-static call skipping: a per-SCC index of the class-static accesses reachable from each call-graph component, a forward skip for calls whose component cannot observe the fact, and the matching backward relevance mirror | Two soundness inversions: an unresolved callee was treated as observing nothing (every Spring `@Autowired` read compiles to an unresolvable stub), and the most general fact — the whole-static-heap access — was judged to observe nothing because the test was containment rather than overlap. **8 lost findings**, plus one compounded with 6.1. Fixing the second inversion re-admits exactly the fact the index existed to prune. | + +Their samples and regression tests were kept and now pin the restored recall. + +## Tier 7 — Added after the same measurement + +| # | Change | Why | +|---|---|---| +| 7.1 | Spring Data custom-fragment override fallback in `JIRCallResolver` | The Tier 3 override-cache re-key is correct, but the old method-only key was the only thing making fragment implementations reachable, by accident. **2 lost findings**, ap-mode independent. Folded into the cache-key commit so the branch never carries the regression. | +| 7.2 | dedicated storage for abstract static edges in `MethodAnalyzerEdges` | Adapted from upstream `cbe3b3ffc`. An edge whose initial and final access are both the depth-0 class-static access carries only an exclusion set; collapsing the family to one exclusion set per statement is where the class-static cost actually goes, without pruning anything. | +| 7.3 | interning of resolved rule objects | Rules resolve per (method, rule) and are retained for the analyzer's lifetime; the exit-sink `anyFunction()` rule matches every method. Retained payload for the duplicated values: 17,295,256 B to 53,520 B over 2000 methods. | + +## Tier 4 — Measured for removal (see results section) + +| # | Change | Rationale for suspicion | +|---|---|---| +| 4.1 | Forward-fallback family: `ExactProcessingTimeBudget` (10 s/vuln), `HybridActionableRuleSelection`, `ForwardActionableRulesRecorder`, `MethodTaintMarkReachabilityIndex`, `TaintRuleMarkFlow`, `relevantForwardActionableRules` | pays an always-on cost (`taintMarks()` on **every** published summary edge in **every** phase) to make a *fallback* cheaper; the 10 s constant makes results non-deterministic | +| 4.2 | Bolt-on caches: `traceResolverCache`, JVM trace-precondition caches, `baseOnlyNDSearchCache` + `modificationVersion`, `baseOnlyMethodCallSummaryHandlers`, `baseOnlyPrepared{F2F,ND}Summaries`, `cachedRawCallResolution` | unbounded per-analyzer retention; ND cache flushes on *any* edge addition so its hit rate may be ~0 | +| 4.3 | ND `emptyDeltaRequired = true` on the **non-BaseOnly** path | the only pruning in the range that changes **full-scan** semantics; unquantified recall risk | +| 4.4 | `alwaysIgnoreMethod(declaredMethod)` made unconditional | deliberate recall trade: `o.toString()` no longer enters project code | + +## Tier 5 — Removed: telemetry and dead scaffolding (no perf benefit, some cost) + +- 20+ BaseOnly counters in `MethodAnalyzer` + `BaseOnlyF2FGroupKindStats` in `UnitRunnerStats` +- the statement-collapse diagnostic map, retaining every initial fact per closure key +- `InterProceduralTraceGraphBuilder.debugInfo()` (+8 key types) — `@Synchronized` on the same + monitor as `process()`, invoked every 10 s for the top-10 active vulnerabilities +- `logger.info` → `logger.debug` for the stats dump, `reportExactTime` (per vuln per stage), + and the mark-reachability filter line (per vuln) +- `TraceSummarizer` / `TraceMetadata` / `shouldMaterializeNode` — inert: no production call + site ever passed a summarizer +- dead members: `SelectedRuleSet.methodCleaner` / `callCleaner`, the footprint index's unused + `reset()` and `Node.context`, `EntryMapper.mapping` visibility, unused `builder` / `origin` + params, unused `BaseOnlyApManager` import +- sink-only SARIF fingerprint (`vulnerabilitySinkHash/v1`) — added purely for A/B comparison + +--- + +# Measured results + +Shared 20-core box. Load average ranged 4-17 during the session, and that turned out to +dominate everything: wall-clock has a roughly +-25% band, and because the phases are +time-boxed, step counts move with load too. Configs were interleaved and the table reports +**min** per cell. `prescan` is the built-in noise gauge — it is identical TreeApManager work in +every config, so any prescan delta between configs that do not touch prescan is pure noise. + +## Headline: baseline vs clean branch (quiet machine, interleaved, min of N) + +| project | config | prescan | shallow | fwd | actionable | full | TOTAL | findings | +|---|---|--:|--:|--:|--:|--:|--:|--:| +| thingsboard | baseline | 46.8s | 62.4s | 50.2s | 10.2s | 15.3s | **125.2s** | 13 or 14 | +| thingsboard | clean | 47.2s | 60.3s | 47.7s | 10.2s | 15.2s | **123.2s** | 14 | +| conductor | baseline | 12.4s | 17.5s | 9.9s | 7.5s | 2.7s | **33.8s** | 4 | +| conductor | clean | 12.1s | 17.4s | 9.8s | 7.0s | 2.7s | **32.7s** | 4 | + +(n=3 per cell.) **Performance-equivalent to slightly better — thingsboard -1.6%, conductor -3.3% — with findings equal or better.** The baseline oscillates between 13 and 14 +thingsboard findings across reps (known time-budget non-determinism); the clean branch returned +14 on every rep observed. + +## Correction: the telemetry strip is not a speed-up + +An earlier pass measured the telemetry strip at -17% thingsboard / -5% conductor. Repeating it +once the machine quietened showed the *baseline* running at 125.2s, i.e. the same as the +stripped build — so that 149.9s baseline figure was load artifact, not signal. The honest claim +for the telemetry removal is **no measurable cost and no measurable benefit at this noise +floor**; it is justified on code-hygiene grounds (and by removing a `@Synchronized` 10-second +graph sweep and an unbounded diagnostic retention map), not on a measured speed-up. + +## Ablations — every suspect was retained + +Measured while the machine was loaded, so the magnitudes are unreliable; the *sign* was +consistent across both projects for all three, and no ablation recovered a finding. + +| project | config | TOTAL | vs stripped | findings | +|---|---|--:|--:|--:| +| thingsboard | stripped | 124.3s | — | 14 | +| thingsboard | minus caches | 159.0s | slower | 14 | +| thingsboard | minus ND empty-delta prune | 153.4s | slower | 14 | +| thingsboard | minus Object-method short-circuit | 150.5s | slower | 14 | +| conductor | stripped | 35.0s | — | 4 | +| conductor | minus caches | 41.4s | slower | 4 | +| conductor | minus ND empty-delta prune | 36.9s | slower | 4 | +| conductor | minus Object-method short-circuit | 41.7s | slower | 4 | + +Nothing here refutes the hypothesis that these three pay for themselves, and none of them buys +recall, so all three stay. They are **not** the "irrelevant caches/hacks" — the telemetry was. + +# Follow-ups not taken (out of scope for this branch) + +1. `TaintAnalysisUnitRunnerManager.newSummaryEdges` populates the taint-mark reachability + index on **every** published summary edge in **every** phase, allocating two `HashSet`s + per fact, but the index is only read on the shallow-scan fallback path. Gating it on + `Phase.ShallowScan` is free work removal; not done here because it could not be measured + above the noise floor on this machine. +2. `shallowRuleSearchExactTimeLimit = 10.seconds` and the phase budget fractions + (0.30 / 0.40 / 0.50 / 0.80 / 0.90) are hardcoded and not configurable. The 10 s constant + makes discovery counts machine-speed dependent. +3. The ND `emptyDeltaRequired = true` prune is the only change in the range that alters + **full-scan** semantics for the default Tree mode. Both benchmarks are unaffected, but it + deserves a wider regression run before merging. +4. `TaintMarkManager` was a plain `HashMap` reached from the rule resolvers, which run + concurrently — fixed here by making it a `ConcurrentHashMap`. The same review found + `cachedRawCallResolution` being read across unit-runner threads while backed by a plain + `Int2ObjectOpenHashMap`; that race is pre-existing and still open. diff --git a/docs/baseonly-conductor-trace-boundary-quotient-2026-07-28.md b/docs/baseonly-conductor-trace-boundary-quotient-2026-07-28.md new file mode 100644 index 000000000..6316d3bb3 --- /dev/null +++ b/docs/baseonly-conductor-trace-boundary-quotient-2026-07-28.md @@ -0,0 +1,107 @@ +# BaseOnly Conductor trace-resolution mitigation + +## Key idea + +Treat an already resolved BaseOnly trace boundary with an implicit-any field as +the canonical representative of otherwise identical concrete-field boundaries. + +The boundary quotient has three parts: + +1. Resolve less field-specific boundaries first. +2. Memoize intra-procedural start-to-final resolution. +3. Reuse a successfully resolved implicit-any result for a concrete-field + request only when the method, statement, trace kind, edge shape, base, + static slot, exclusions, suffix, taint mark, and value-suffix mode are + identical. Only the field slot may change from implicit-any to concrete. + +An empty result is not used to cover another request. Non-BaseOnly facts and +non-deterministic edges require exact equality. + +This removes repeated traversal of the same summary graph for boundaries that +represent the same BaseOnly suffix semantics. It does not generalize stored +forward facts or summary edges. + +The quotient depends on the trace representation and BaseOnly trace invariants +that were present in the successful experimental worktree but were accidentally +omitted from commit `89cddda88`: + +1. Equivalent action edges are stored once, with action alternatives kept as + variants outside the graph entry. +2. BaseOnly backward trace facts have empty exclusions. Forward exclusions are + applied by `FinalFactAp.contains` when matching an entry; copying them into + backward facts multiplies semantically equivalent trace states. +3. A concrete-field call summary is discarded only when an applicable + implicit-any summary with the same conclusion covers it. +4. BaseOnly containment checks the first concrete accessor after an abstraction + point against the forward fact's exclusions. +5. `ActionVariant` caches its immutable edge set and structural hash. + `createActionOrContinuationEntry` receives an already deduplicated set and + copies it directly instead of calling `distinct()` and deeply hashing every + variant a second time. + +The complete mitigation requires these invariants as well as the boundary +quotient. Testing the quotient on top of an unstaged worktree masked this +dependency during the original commit validation. + +## Isolation and repeated-run result + +The projected call-summary shortcut, caller-trace antichain, F2F field +generalizer changes, and summary-storage changes were removed. + +The first clean candidate exposed two errors in the earlier validation: + +- without the omitted prerequisites, the committed quotient remained slow; +- after restoring them, rule-search trace resolution was stable, but path + resolution still depended on hash-derived action-variant order; +- experimentally preferring a non-summary variant reduced typical time but did + not eliminate the timeout without diagnostic logging, so that behavior + change was removed from the final patch. + +A thread dump from the remaining 18/19 stall showed the only running worker in +`MethodTraceResolver.TraceBuilder#createActionOrContinuationEntry`. It had used +about 146 CPU-seconds inside `variants.distinct()`, recomputing +`ActionVariant -> Sequential -> Set` hashes. The argument was already +a `LinkedHashSet`, so this was duplicate work rather than semantic +deduplication. + +After removing the redundant `distinct()` and caching immutable variant state, +two independent no-probe Conductor scans of the final hash-only candidate +completed without timeout or OOM. Each produced 19 shallow discoveries and +completed all four relevant batches: + +| run | rule-search trace | actionable entries | final trace | path trace | +|---|---:|---:|---:|---:| +| 1 | 19/19 | 19/19 | 19/19 | 19/19 | +| 2 | 19/19 | 19/19 | 19/19 | 19/19 | + +Path resolution completed in 23.3 s and 10.0 s respectively. + +## False-negative analysis + +The change should not introduce a false negative if the BaseOnly abstraction +obeys its intended ordering: for the same suffix semantics, an implicit-any +field boundary covers every concrete-field boundary. Resolving the covering +boundary is then an over-approximation of resolving the covered boundary. +Scheduling and exact memoization do not change reachability. + +Caching the hash and edge set does not change equality, and replacing +`variants.distinct()` with `variants.toList()` does not change membership: +the caller constructs and passes a `Set`. These changes remove +only repeated computation. + +The implementation deliberately prevents the known unsafe variants: + +- it never drops or changes the suffix, taint mark, or value-suffix mode; +- it never generalizes static access; +- it never changes edge arity or pairs different statements/methods; +- it does not use an empty weak result to suppress concrete resolution; +- it reuses only a weak boundary that was actually resolved, rather than + synthesizing one. + +The remaining semantic dependency is monotonicity of backward trace transfer: +every concrete-field predecessor/action must also be present when resolving the +covering implicit-any boundary. If a future operation treats implicit-any more +narrowly than a concrete field, reuse could hide that concrete trace. The +field-generalization law tests protect the boundary relation itself; scenario +tests should continue comparing BaseOnly reachability and collected actions +against Tree for field-sensitive flows. diff --git a/docs/baseonly-storage-spec.md b/docs/baseonly-storage-spec.md new file mode 100644 index 000000000..2d6cb946b --- /dev/null +++ b/docs/baseonly-storage-spec.md @@ -0,0 +1,587 @@ +# BaseOnly storage specification + +## Status and scope + +This document is the normative specification for storage owned by the BaseOnly access-path implementation. It covers intraprocedural edge sets, method-summary stores, side-effect stores, subscriptions, the final-fact stack, and the indexes used by those stores. + +The BaseOnly access-domain specification defines canonical accesses and these semantic operations: + +- `concretize(A)`: the concrete Tree paths denoted by `A`; +- `covers(A, B)`: directional inclusion, `concretize(B) ⊆ concretize(A)`; +- `mayOverlap(A, B)`: symmetric non-empty intersection; +- `residual(P, F)`: the deltas by which `F` extends a matched pattern `P`. + +This document does not redefine those operations. A storage must call their shared production implementation. Packed-slot compatibility is not a storage relation. + +Tree is the behavioral reference. BaseOnly may store or return a less precise representation, but for the same inserted projected edges it must not omit behavior returned by Tree: + +```text +project(denotation(Tree result)) ⊆ denotation(BaseOnly result) +``` + +This is a denotational requirement. Tree may merge several paths into one access tree while BaseOnly returns several records, or conversely BaseOnly may return one widened record. Collection shape and iteration order are not observable semantics. + +The words **must**, **must not**, **should**, and **may** are normative. + +## Terms + +### Fact and edge identity + +A fact is the tuple: + +```text +Fact = (base, canonical access, exclusions) +``` + +An edge contains the bases and accesses named in the per-storage tables below. Exclusions are edge payload unless explicitly included in a logical key. Statement and method/exit-point partitioning performed by common storage wrappers is part of the key even when it is outside the BaseOnly leaf structure. + +Two records are the same logical record when all key components are equal after canonicalization. Object identity, packed construction history, insertion order, bucket, and normalized-view origin are never key components. + +Value-accessor state is part of canonical access identity. For the same compact +semantic accessor, `Normal` and `Value` are distinct keys and denote different +paths. An index may route them through the same suffix bucket, but must compare +the full access before lookup or publication. + +### Record denotation and subsumption + +The denotation of a stored record is the set of concrete Tree facts or edges represented by its canonical BaseOnly access values and exclusions. A record `R1` subsumes `R2` exactly when: + +```text +denotation(R2) ⊆ denotation(R1) +``` + +Directional `covers` is used to prove access inclusion inside this definition. `mayOverlap` cannot prove subsumption. + +A store may physically retain a subsumed record, but one collection must not emit duplicate logical behavior. Physical pruning is an optimization and must preserve the single-writer/multiple-reader publication rules. + +### Query applicability + +For a nullable access pattern `P` and stored initial access `I`: + +```text +applicable(P, I) = P == null || mayOverlap(P, I) +``` + +This is the authoritative predicate for F2F-summary and fact-side-effect lookup. It captures Tree's `filterContains`: a Tree query can select a stored prefix or a stored descendant represented by an abstract pattern. The BaseOnly result must include every projected Tree result. Exclusions remain attached to returned records and do not remove index candidates. + +An index may return a strict superset of applicable records. Every candidate must pass `applicable` before emission. An index must never use `covers` in one arbitrary direction as a substitute for overlap. + +### Transient collapsed values + +`COLLAPSED_MARK` belongs only to the `removeAbstraction`/`rebase` flow-function +lifecycle. Every BaseOnly insertion boundary checks `access.isCollapsed` before +mutating keys, payloads, deltas, indexes, or subscriptions. Stable accesses are +inserted directly; there is no probing predicate and no exception-driven +classification. The common storage implementations remain unchanged. + +## Exclusion algebra + +`ExclusionSet` has the ordinary set order: + +```text +Empty ⊆ Concrete ⊆ Universe +``` + +`union` and `intersect` mean set union and intersection. Storage merge direction follows the denotation of the record, not a universal rule. + +### Alternative-flow merge + +When two records in the same logical edge aggregate describe alternative executions, their represented behaviors are united. For an open access with exclusions, the union of the two allowed languages excludes only accessors excluded by both alternatives. Therefore: + +```text +mergeAlternative(E1, E2) = E1 intersect E2 +``` + +This applies to method F2F summaries, including identity and non-identity summaries. It matches Tree's identity exclusion merge and Tree's per-initial non-identity summary merge. + +### Fact-state merge + +When two intraprocedural F2F facts at the same statement/key accumulate known exclusions as fact state, the stored exclusion is: + +```text +mergeFactState(E1, E2) = E1 union E2 +``` + +This matches `MethodEdgesInitialToFinalTreeApSet`. + +### Side-effect merge + +For the same fact-side-effect key or side-effect-requirement key, exclusions accumulate by union: + +```text +mergeSideEffect(E1, E2) = E1 union E2 +``` + +This matches Tree side-effect summary and requirement storage. + +### Merge laws + +Every merge operator used by a storage must be associative, commutative, and idempotent. Consequently, final state is independent of batch boundaries and insertion order. + +When stored alternatives contain paths with the same prefix and semantic +accessor but different value-accessor states, they remain separate records: + +```text +Normal join Normal = { Normal } +Value join Value = { Value } +Normal join Value = { Normal, Value } +``` + +The union is represented by two facts, never by a third packed state. Query +matching, subsumption, deltas, and normalized aliases process and preserve each +state independently. + +If a batch updates one logical aggregate more than once, the persistent storage +merges each update immediately and coalesces writer-local delta keys until the +batch is drained. A logical delta may require several builders when the +final-access language is materialized as several canonical accesses; each +required component is emitted at most once. The drained delta contains the full +final-access language with the final aggregate for exclusion-only changes, and +may contain only the newly admitted final-access behavior when the exclusion is +unchanged. It must never contain an intermediate exclusion value. + +## Ownership and concurrency + +### Summary and side-effect stores + +Method Z2F, F2F, and ND summary stores, fact-side-effect stores, and side-effect-requirement stores use this contract: + +```text +one writer; zero or more concurrent readers; eventually consistent +``` + +The effective writer is serialized by the analyzer. Readers do not acquire the writer monitor. + +A reader may observe an older complete state and may omit an insertion concurrent with that query. A later query after writer completion must observe the committed insertion. A reader must never observe: + +- a value before all required fields are initialized; +- a transient default such as `Universe` that was never committed; +- a key paired with a value from another table generation; +- a malformed/null record; +- a transient collapsed record rejected before insertion; +- two emissions of the same logical view in one query; +- an exception caused by concurrent insertion or rehash. + +Shared indexes are append-only under this contract. Concurrent-read-safe primitive maps/sets must use captured-generation point reads and traversal. Inherited fastutil iterators are forbidden. Values must be fully initialized before the key or parent link is published. A mutable value visible to readers must publish complete immutable replacements, or use a holder with an equivalent proven publication protocol. + +Subsumption must not physically remove an entry from an append-only SWMR index. It must use immutable replacement/tombstone state that readers can interpret safely, or leave the entry in place and suppress it with the authoritative denotational check. Rebuilding and atomically publishing an immutable root is also valid. + +Delta accumulators are writer-owned and are never read concurrently. They use ordinary collections and are drained once per writer batch. + +### Intraprocedural fact sets and final-fact lists + +Intraprocedural Z2F/F2F/ND edge sets and `FinalFactList` are single-analysis-thread-owned. Ordinary primitive maps, sets, arrays, and lists are correct. Adding concurrent structures to these stores is not required and must not alter their semantics. + +### Subscription registries + +Subscription registration and collection are analysis-thread-owned under the current workload. Ordinary collections are correct. If subscription lookup is later moved to concurrent readers, that is a contract change and requires the SWMR rules above; it must not be inferred from summary-store concurrency. + +## Shared initial-access index + +`BaseOnlyInitialAccessIndex` is a candidate index, not a semantic store. Its logical key is one canonical initial access. It must provide: + +```text +getOrCreate(I) +collectAll() +collectCandidates(P) +``` + +The key preserves the complete canonical access, including value-accessor state. Slot projections +such as `(staticIdx, fieldIdx, suffixIdx)` are routing dimensions only and must not merge accesses +whose packed value-accessor states differ. + +`collectCandidates(P)` must be complete: + +```text +applicable(P, I) implies I is visited +``` + +It may visit non-applicable `I`. Callers must apply `applicable(P, I)` after traversal. The index must not merge different canonical keys, own exclusions, emit deltas, or normalize accesses. + +Tree comparison: Tree's `AccessBasedStorage.filterContains` is the semantic reference, including stored prefixes, abstract-pattern descendants, and Any behavior. For bounded projected Tree inputs, every Tree-selected initial must occur in the BaseOnly candidate set and pass the BaseOnly authoritative predicate. + +Index laws: + +- exact lookup returns the value for the exact canonical key; +- full collection returns every published key at most once; +- patterned collection is complete for `applicable`; +- insertion and candidate results are independent of insertion order; +- after the writer completes, indexed candidates equal a scan-and-predicate reference after authoritative filtering; +- a query during rehash returns a subset of one or more complete published generations, never a malformed pair. + +## Intraprocedural storage + +These stores are single-threaded and keyed by the common method edge partitions plus the BaseOnly components below. + +| Store | BaseOnly logical key | Merge/result | Tree relation | +|---|---|---|---| +| Z2F edge set | `(statement, final base, final access)` | Exact set union; exclusions are `Universe` | Denotation equals or covers Tree's merged access tree at the statement | +| F2F edge set | `(statement, initial base, initial access, final base)` with final-access language as payload | Union final-access denotations; one aggregate exclusion merged with `mergeFactState` | Covers Tree's per-initial merged final tree and uses Tree's exclusion union | +| ND edge set | `(statement, final base, canonical set of initial facts with Universe exclusions)` with final accesses as payload | Exact set/denotational union of finals | Covers Tree's merged final tree for the same initial set | +| Final-fact list | stack position | Preserve exact `(base, access, exclusions)`; LIFO remove | Same ordered stack behavior as Tree; no access merge | + +Collapsed values contribute nothing. `add` returns no delta when the inserted denotation is already represented. If BaseOnly retains multiple accesses where Tree returns one tree, collection returns their denotational union; callers must not depend on cardinality or order. + +Intraprocedural F2F lookup with an explicitly supplied initial uses exact canonical initial access, as Tree does. A normalized summary alias is not an intraprocedural fact-set key and must not be stored in this set. If trace resolution needs an alias, it is applied at summary query time. + +## Method-summary storage + +Method summaries are partitioned by method entry/exit point and bases in the common layer. The following sections specify the BaseOnly leaf state. + +### Z2F summaries + +Logical key: + +```text +(final base, final access) +``` + +Exclusions are `Universe`. Insertion is denotational set union. The writer emits a delta only for newly admitted final-access behavior. Collection returns every current logical final once. + +Tree comparison: Tree merges all Z2F finals for the same partition into one access tree. The union of BaseOnly results must cover the projection of that tree. BaseOnly result cardinality is not required to equal Tree cardinality. + +### F2F summaries + +The primary non-identity aggregate key is: + +```text +(initial base, initial access, final base) +``` + +Its payload is the union of final-access languages and one exclusion value merged with `mergeAlternative` across every alternative in the aggregate. BaseOnly may materialize that language as several canonical final accesses, but every emitted component reads the aggregate's current exclusion. It must not retain a different exclusion per exact final: that would preserve a correlation that Tree deliberately loses when it merges the final tree and intersects exclusions. + +The identity aggregate key is `(initial base, initial access, final base)` plus the fact that its payload denotes the identity portion extracted from the final language. Identity is an optimization class only; it does not define a different exclusion algebra or query relation. + +For each writer batch: + +1. canonicalize and validate all accesses; +2. split identity and non-identity behavior by the shared access operation; +3. incrementally merge each edge into its persistent aggregate; +4. update candidate/subsumption indexes as part of that aggregate insertion; +5. record the changed persistent aggregate in writer-local delta state; +6. after all inputs are stored, emit one logical primary delta per changed + aggregate, materialized as each required final-access component exactly once. + +No temporary batch aggregate duplicates the persistent identity trie or +non-identity merging storage. The persistent structures are the sole source of +merge and subsumption semantics. + +Identity summaries follow Tree's exclusion-aware hierarchical subsumption. +Repeated insertion of the same canonical access intersects exclusions. An +abstract access suppresses a concrete identity only when the concrete accessor +is a child in that same packed/logical slot and is not present in the abstract +edge's exclusions. A `NO_ACCESSOR` advances to a later slot and is not a child +edge, so `(NO_ACCESSOR, field-AP, NO_ACCESSOR)` does not subsume +`(NO_ACCESSOR, NO_ACCESSOR, suffix)`. Normal and Value suffix children are +distinct keys; a suffix abstraction may suppress both when their shared +semantic accessor is permitted. Patterned collection obtains conservative +candidates and applies `mayOverlap`. + +Patterned collection uses `applicable(pattern, storedInitial)`. The initial-access index only chooses candidates; the final predicate is mandatory. Full collection uses a null pattern. Each materialized component `(aggregate identity, final access, aggregate exclusion)` is emitted at most once. + +Tree comparison: + +- Tree detects identity behavior with `splitOnMatching` and stores it in an exclusion-aware trie. BaseOnly must cover that identity denotation whether it classifies the record as identity or non-identity. +- Tree merges non-identity final trees per initial and intersects alternative-flow exclusions. The union of BaseOnly components for that initial must cover the projected Tree summary, and all components must expose the same merged exclusion. +- Tree's `filterContains` determines pattern applicability. BaseOnly must include every projected result and may include only results allowed by `mayOverlap`. + +### Normalized F2F aliases + +A normalized access is a query-time view of one primary F2F record. It is not a second summary record. + +For a materialized primary component `R`, normalization may produce zero or more exposed initial accesses `aliasInitial(R)`. A collected view has identity, within the common base/exit partitions: + +```text +(exposed initial access, primary final-access component) +``` + +The alias: + +- owns no exclusion state; +- reads the current exclusion from its primary record; +- owns no delta accumulator; +- emits no insertion/update delta; +- owns no independent subscription state; +- cannot outlive or diverge from its primary record; +- participates in a conservative trace-query candidate view; +- is generated by one shared normalization operation. + +Primary and alias views with the same exposed initial/final are emitted once. If +several primary aggregates expose that same view, their exclusions are intersected +as alternative flows at collection time; the alias still owns no independent state. +A primary and alias view with different exposed initials may both be emitted because +they are distinct trace alternatives. + +Trace-query collection may scan all primary components in the selected method/base storage and +emit both primary and alias views even when the packed query pattern does not satisfy the ordinary +forward `applicable` predicate. This is required because the projected query can match an alias +whose primary initial is outside the packed candidate bucket. The backward trace resolver's +entry-edge containment/residual check is authoritative. This conservative view does not change +primary state, forward deltas, or forward subscription fan-out. + +Alias availability is selected by the analyzer's explicit one-way transition from +forward queries to trace-resolution queries. Each storage query captures that +phase once at entry, so a transition cannot change the meaning of an +already-running query. There is no general-purpose mutable alias toggle. + +Tree comparison: aliases exist only to preserve a Tree-resolvable backward match lost by BaseOnly projection. Adding an alias must not add a forward summary delta or a second forward fact. For each Tree summary applicable to a query, the primary/alias view union must contain an applicable BaseOnly view. + +### ND F2F summaries + +Logical key: + +```text +(final base, canonical set of initial facts with Universe exclusions, final access) +``` + +Initial-set equality is order-independent. Repeated final accesses are idempotent. A writer batch emits each newly admitted final-access behavior once for its initial set. + +A query with no initial pattern scans all initial sets. A query with pattern base `B` considers only initial sets containing an initial fact with base `B`; access-level filtering of each returned final then follows the summary application operation, not an unrelated base-wide broadcast. If the public query provides enough access information to filter before return, the implementation should use it, but filtering must remain a complete overapproximation of Tree. + +Tree comparison: Tree indexes initial facts by base and merges final trees per equal initial set. BaseOnly must select every Tree-relevant initial set and its final-access union must cover the projected Tree final tree. + +## Side-effect storage + +### Fact-side-effect summaries + +Logical key: + +```text +(initial base, initial access, side-effect kind) +``` + +Repeated exclusions merge with `mergeSideEffect`. A batch emits at most one final aggregate per changed key. Patterned lookup uses `applicable(pattern, initialAccess)` through the shared initial-access index and authoritative predicate. Null pattern performs a full scan. + +Tree comparison: Tree uses `AccessBasedStorage.filterContains` and union-merges exclusions per kind. BaseOnly must return every projected Tree-selected side effect with an exclusion set that does not remove Tree behavior. + +### Side-effect requirements + +Logical key: + +```text +(required base, required initial access) +``` + +Repeated exclusions merge with `mergeSideEffect`. `add` applies requirements +incrementally in input order and drains each modified storage's accumulated +delta after insertion; it does not pre-coalesce the input batch. +`collectAllRequirementsTo` returns every current logical requirement once. + +`filterTo(fact)` first selects the exact base, then returns only requirements whose initial access is applicable to the fact's final access. It must not broadcast every requirement for the base. + +Tree comparison: Tree calls `filterContains(fact.access)` and returns only matching requirement nodes. The BaseOnly result must include every projected Tree match and must pass the BaseOnly `mayOverlap` predicate. + +## Subscription storage + +Subscriptions store caller edges waiting for a callee summary whose initial fact is `P`. Registration deduplicates the full caller-side logical key; it does not merge unrelated caller initials or exits. + +All Z2F, F2F, and ND lookup uses one shared candidate operation: + +```text +subscriptionCandidates(registrations, P, mode) -> superset of applicable registrations +``` + +The result is a candidate set, not a semantic partition. It must contain every registration that +Tree can select. BaseOnly may conservatively return additional registrations because several +distinct Tree exit branches and residual classes project to one packed access. In particular, +`emptyDeltaRequired` must not be used to discard a projected candidate when BaseOnly cannot prove +that every represented Tree branch belongs to the opposite class. The downstream residual/concat +operation is authoritative and rejects or specializes candidates after subscription delivery. + +Registration deduplication remains exact. Candidate broadcast is therefore bounded by the +registrations for the selected method/base storage; it is not permission to cross caller endpoint, +callee base, or caller final base partitions. + +### Z2F subscriptions + +Logical registration key: + +```text +(callee initial base, caller endpoint, caller final base, caller exit access) +``` + +Lookup emits a conservative candidate superset for the selected registration storage. Every Tree +`filterStartsWith` result must be present; the downstream residual operation remains authoritative. + +### F2F subscriptions + +Logical registration key: + +```text +(callee initial base, caller endpoint, caller final base, + caller initial fact including exclusions, caller exit access) +``` + +Lookup uses the shared candidate operation for both values of `emptyDeltaRequired`. Returned +builders preserve the exact registered caller initial fact and its exclusions. Tree currently +ignores the flag at this lookup boundary; BaseOnly may do the same because partitioning the merged +projection is unsound. The later residual operation still observes the requested analysis mode. + +### ND subscriptions + +Logical registration key: + +```text +(callee initial base, caller endpoint, caller final base, + canonical set of caller initial facts normalized to Universe exclusions, + caller exit access) +``` + +Relevant-storage indexing must be complete for the candidate relation. Each selected registration +group may conservatively return all of its exits. `emptyDeltaRequired` has the same candidate-only +meaning as for F2F and must not remove a projected Tree match. + +Tree comparison: Tree uses a final-access prefix index and `filterStartsWith`; Automata uses graph +localization plus `delta`/containment. BaseOnly may use its own index or scan the selected logical +registration group. Its emitted set must cover projected Tree matches; extra candidates are allowed +and are discharged by downstream residual processing. + +## Publication and delta protocol + +All SWMR stores follow this insertion protocol: + +1. The writer canonicalizes and validates input without mutating shared state. +2. For each input, it computes the next persistent aggregate value. +3. It fully initializes a new leaf value or immutable replacement. +4. It publishes the leaf before or atomically with publishing its index key, according to the proven concurrent-read-safe collection protocol. +5. It updates secondary candidate indexes only with references to complete primary values. +6. It records the aggregate key in writer-local delta state. +7. After processing the batch, it reads the final persistent aggregate for each + changed key, emits each required materialized component once, and clears + writer-local delta state. + +Readers resolve secondary entries back to the primary record and recheck the authoritative relation. A secondary index never becomes a source of truth. + +Delta laws: + +- inserting an already represented record emits no delta; +- reordering a batch does not change primary state or emitted logical delta set; +- splitting a batch may change when deltas are observed, but the union of emitted behavior equals the one-batch result; +- alias creation emits no delta; +- an exclusion-only update emits the committed aggregate, not a transient value; +- no delta is retained indefinitely after its batch is drained. + +## Differential and reference laws + +Every storage must be tested against a synchronized, scan-based reference implementation that stores canonical logical records directly and uses the definitions in this document. Tests compare denotations and logical keys, not iteration order. + +For every bounded set of Tree records `T`, projection `project`, query `Q`, and BaseOnly result `B`: + +```text +project(collectTree(T, Q)) ⊆ denotation(collectBaseOnly(project(T), project(Q))) +``` + +Required deterministic laws: + +- duplicate insertion is idempotent; +- final state and logical deltas are insertion-order independent; +- all exclusion merge operators satisfy their declared algebra; +- candidate index plus authoritative filtering equals a full scan; +- identity and non-identity F2F storage implement the same edge denotation; +- distinct cross-slot identity records survive in both insertion orders; +- `Normal` and `Value` keys remain distinct through insertion, + normalization, lookup, and joining; a join returns both facts in either + insertion order; +- primary and normalized views share one exclusion value and aliases emit no deltas; +- each subscription mode returns a candidate superset of the corresponding Tree registrations; +- side-effect filtering never broadcasts a non-applicable same-base key; +- transient collapsed values have no observable effect; +- single-thread fact stores cover the corresponding Tree merged result. +- subscription registration before and after the analyzer makes summaries + available returns the same conservative candidate language; subscriptions + themselves remain analysis-thread-owned. + +Required deterministic SWMR release schedules: + +- read during first insertion; +- read during every index/table rehash; +- read between value initialization and key publication; +- read during exclusion aggregate replacement; +- two updates to one key in one writer batch; +- subsuming identity inserts in both orders; +- primary and normalized lookup overlap. + +After the writer joins, collection must equal the reference state. During +writing, every observed record must belong to some complete committed prefix of +writer insertions; a reader may therefore observe an aggregate between two +inputs of the same writer batch. Batch boundaries govern delta draining, not +reader visibility. + +The current tests establish the sequential laws above and exercise first-leaf, +rehash, and aggregate-replacement publication with concurrent stress loops. They +do not deterministically pause a reader at every publication boundary. Dedicated +scheduled tests are also still required for method Z2F, method ND, +fact-side-effect, and side-effect-requirement stores. Consequently, the semantic +storage operations may be Perfect below while the cross-cutting SWMR evidence +gate remains open; stress coverage alone is not proof of every required +interleaving. + +## Per-storage conformance matrix + +| Component | Ownership | Semantic reference | Required BaseOnly predicate/algebra | +|---|---|---|---| +| `BaseOnlyInitialAccessIndex` | SWMR when used by summaries | Tree `AccessBasedStorage.filterContains` | candidate superset, then `mayOverlap` | +| Intraprocedural Z2F set | single thread | Tree merged statement fact tree | exact/denotational set union | +| Intraprocedural F2F set | single thread | Tree per-initial statement store | exact initial; final union; exclusion union | +| Intraprocedural ND set | single thread | Tree per-initial-set merged tree | exact initial set; final union | +| `FinalFactList` | single thread | common/Tree list | exact LIFO tuple preservation | +| Method Z2F summaries | SWMR | Tree merging Z2F tree | final denotational union | +| Method F2F identity summaries | SWMR target; proof postponed | Tree identity trie | layered null-tombstone subsumption in the same slot; state-distinct suffix leaves; `mayOverlap` query | +| Method F2F non-identity summaries | SWMR | Tree per-initial merging store | `mayOverlap` query; exclusion intersection | +| Normalized F2F view | query-time SWMR read | Tree-resolvable backward match | primary-backed alias; no state/delta | +| Method ND summaries | SWMR | Tree initial-base index + merged finals | exact initial set; relevant-base completeness | +| Fact-side-effect summaries | SWMR | Tree filtered initial trie | `mayOverlap`; exclusion union | +| Side-effect requirements | SWMR | Tree `filterContains` | `mayOverlap`; exclusion union | +| Z2F subscriptions | analysis thread | Tree filtered caller-exit tree | conservative candidate superset; downstream residual authoritative | +| F2F subscriptions | analysis thread | Tree/Automata filtered caller exits | conservative candidates for either requested mode | +| ND subscriptions | analysis thread | Tree/Automata relevant-exit index | complete registration-group candidates | + +## Mitigation verdict ledger + +Verdicts are release gates, not statements about representation equality. **Perfect** means the +specification is general, the implementation follows it, and the cited bounded Tree differential +or storage law establishes that BaseOnly does not underapproximate the reference scenario. + +| Component/API operation | Verdict | Tree-relative evidence | +|---|---|---| +| `BaseOnlyInitialAccessIndex.getOrCreate` / exact lookup | Perfect | `BaseOnlyInitialAccessIndexTest`; exhaustive value-accessor-state exact-key and duplicate laws | +| `BaseOnlyInitialAccessIndex.collectAll` / patterned candidates | Perfect | `BaseOnlyInitialAccessIndexTest`; `BaseOnlyF2FSummaryStorageLawTest.patterned query equals a scan-and-predicate reference` | +| Intraprocedural Z2F `add` / collect-all / patterned collect | Perfect | `BaseOnlyTreeDifferentialStorageTest.intraprocedural Z2F F2F and ND sets cover Tree collection and deltas`; `BaseOnlyFactSetTest` | +| Intraprocedural F2F collect-all / final-base pattern / exact-initial collect | Perfect | bounded differential scenario plus `BaseOnlyFactSetTest.f2f shares Tree fact-state exclusion union across its final language` and `f2f exclusion update retains Normal and Value finals separately` | +| Intraprocedural F2F `add` delta on exclusion-only aggregate change | Perfect | list-valued `MethodEdgesInitialToFinalApSet.add` re-emits every stored final with the merged exclusion; `MethodEdgesInitialToFinalApSetTest` covers Tree, Automata, Cactus, and BaseOnly, while `BaseOnlyFactSetTest` covers structural and Normal/Value final pairs plus publication through `MethodAnalyzerEdges` | +| Intraprocedural ND `add` / collect-all / final-base pattern / exact-initial-set collect | Perfect | bounded differential scenario plus `BaseOnlyFactSetTest.nd f2f canonicalizes initial exclusions before key publication` | +| `BaseOnlyFinalFactList.add` / `get` / `removeLast` | Perfect | Tree differential LIFO scenario plus rejected-transient/no-array-shift law in `BaseOnlyFactSetTest` | +| Method Z2F summary `add` / base-filtered and all-base collect | Perfect | `BaseOnlyTreeDifferentialStorageTest.method Z2F F2F and ND summary queries cover Tree`; duplicate/idempotence laws inherited from the exact set | +| Method F2F identity `add`, subsumption, merge, and delta | Perfect sequential semantics; SWMR evidence postponed | cross-slot, same-slot abstraction, exclusion, insertion-order, and value-state laws in `BaseOnlyF2FSummaryStorageLawTest` | +| Method F2F non-identity `add`, merge, and delta | Perfect | `BaseOnlyF2FSummaryStorageLawTest.nonidentity exclusion aggregation is intersection and insertion-order independent`; value-accessor-state key/candidate law; repeated-batch aggregate law; new-final/aggregate-exclusion publication stress coverage | +| Method F2F null-pattern and patterned collect | Perfect | `BaseOnlyF2FSummaryStorageLawTest.patterned query equals a scan-and-predicate reference`; bounded Tree F2F summary scenario | +| Method F2F normalized-view collect | Perfect | `BaseOnlyF2FSummaryStorageLawTest.normalized alias emits no delta and reads the primary exclusion`; alias/exact-primary dedup law | +| Method ND summary `add` / null-pattern and initial-base-pattern collect | Perfect | `BaseOnlyTreeDifferentialStorageTest.method Z2F F2F and ND summary queries cover Tree` | +| Fact-side-effect `add` / null-pattern and patterned collect | Perfect | `BaseOnlyTreeDifferentialStorageTest.fact side effects and requirements cover Tree filtering and exclusion union`; `BaseOnlyInitialAccessIndexTest` scan reference | +| Side-effect requirement `add` / collect-all / `filterTo` | Perfect | same bounded Tree differential scenario; `BaseOnlySubscriptionAndReqTest.side effect requirement filtering equals a scan reference` | +| Z2F subscription register / collect candidates | Perfect | `BaseOnlyTreeDifferentialStorageTest.Z2F F2F and ND subscriptions cover Tree residual modes`; candidate-superset law | +| F2F subscription register / empty and non-empty candidate collect | Perfect | same bounded differential scenario; `BaseOnlySubscriptionAndReqTest` conservative scan laws | +| ND subscription register / empty and non-empty candidate collect | Perfect | same bounded differential scenario; `BaseOnlySubscriptionAndReqTest` conservative scan laws | +| Cross-cutting SWMR publication evidence | Postponed known issue | current F2F/index stress tests do not deterministically force all required boundaries, identity null-tombstone publication has not been proven under every reader schedule, and dedicated concurrent-reader schedules are missing for Z2F, ND, fact-side-effect, and side-effect-requirement stores | + +The differential suite intentionally compares bounded readable path languages instead of record +counts: Tree merges branches into access trees while BaseOnly may expose several records. Summary +stores and side-effect stores remain SWMR; the new differential scenarios are sequential reference +checks and therefore do not weaken or replace the deterministic concurrent-publication laws. + +## Resolved representation/interface decisions + +1. **Normalized-query control.** Resolved by the explicit one-way analyzer phase transition described above. A future common-interface query-mode parameter could make the phase local to a call, but is not required for correctness under the current forward-then-trace workload. +2. **Collapsed operational sentinel.** The access-domain specification permits it only in a + transient final fact between `removeAbstraction` and `rebase`. Each BaseOnly insertion method + rejects `access.isCollapsed` before calling or mutating its storage. Serialization validates + the state directly. It is never a storage key or payload. +3. **Common-wrapper publication.** Common storage code is unchanged. Where a common wrapper would + publish parallel metadata before the BaseOnly payload, the BaseOnly subtype overrides the + public insertion method and rejects a collapsed access before delegating. BaseOnly otherwise + uses the same confirmed concurrent-read-safe lazy wrappers as Tree/Automata. +4. **ND prefilter strength.** Base membership selects the logical registration group. ND + subscriptions conservatively emit that group's exits for either mode; downstream residual + processing is the authoritative filter. + +These decisions do not waive Tree coverage, authoritative filtering, initialized publication, or no-delta alias requirements. diff --git a/docs/baseonly-subscription-and-polymorphic-proxy-design.md b/docs/baseonly-subscription-and-polymorphic-proxy-design.md new file mode 100644 index 000000000..b65272f59 --- /dev/null +++ b/docs/baseonly-subscription-and-polymorphic-proxy-design.md @@ -0,0 +1,64 @@ +# BaseOnly subscription and polymorphic-call mitigation + +## 1. Delta-sound subscription routing + +`MethodBaseOnlyAccessPathSubscription` is a candidate index. It may return a false-positive +subscription, but it must not reject a subscription that the canonical summary operation can +apply. + +For a registered caller exit `F` and a newly published summary initial `I`, the authoritative +access-level predicate is: + +```text +M = BaseOnlyAccessOps.matchPrefix(F, I) + +ordinary summary event: M.emptyDelta || M.hasSuffix +empty-delta event: M.emptyDelta +``` + +This is the same match used by `BaseOnlyFinalFactAp#delta`. Exclusions remain a downstream concern: +the index may retain a suffix candidate that a summary-initial exclusion later removes. + +The packed three-slot `BaseOnlyInitialAccessIndex` supplies a conservative candidate set. F2F +subscriptions are inverted by caller exit so one index lookup selects all caller initials attached +to an applicable exit. ND subscriptions additionally map each exit to the initial-set storage +indices that contain it. Z2F uses the same exit index directly. Every emitted candidate is checked +with the predicate above. + +The index and its leaf values follow the existing single-writer/multiple-reader contract: +three-slot maps and long sets are concurrent-read-safe; object sets and storage-index bitsets use +copy-on-write publication. + +## 2. Polymorphic resolution + +`JIRCallResolver` returns every contextual concrete and lambda alternative. +`JIRMethodCallResolver` processes those results directly; it does not insert a synthetic summary +method between the caller and the resolved targets. + +A resolution-set proxy was evaluated and rejected. Its additional method-summary boundary merged +the results of broad generic dispatches such as `FutureCallback.onFailure` and +`DataValidator.validateDataImpl`. Contextual target sets also fragmented the proxy cache: one +source statement could produce dozens of synthetic methods, while most generated proxies were +used only once. + +The ThingsBoard experiment measured the consequence: + +- direct resolution: 104.6s prescan, 35.0s full scan, 14 findings, no high-memory events; +- resolution-set proxy: 109.5s prescan, 68.4s full scan, 13 findings, 32 high-memory events; +- compact one-statement proxy: 113.0s prescan, 95.1s full scan, 14 findings, 56 high-memory events. + +Changing the proxy CFG did not remove the regression. The expensive operation was aggregating a +broad target set into another summary and then applying that merged summary to callers. +Direct resolution preserves each `MethodWithContext`, keeps lambda subscription in the original +caller context, and avoids that extra aggregation boundary. + +## Verification + +- Subscription tests compare F2F and ND results with a canonical-delta scan for ordinary and + empty-delta events. +- Packed-shape index tests assert that routing contains every pair accepted by canonical delta. +- Tree/BaseOnly differential storage tests assert BaseOnly does not drop the corresponding Tree + subscription. +- Dataflow samples cover direct resolution of two concrete implementations and a + concrete-plus-lambda implementation in BaseOnly mode, in addition to the existing identity, + transforming, captured, and passed-lambda cases. diff --git a/docs/baseonly-summary-edge-filter-design.md b/docs/baseonly-summary-edge-filter-design.md new file mode 100644 index 000000000..dcb0eb386 --- /dev/null +++ b/docs/baseonly-summary-edge-filter-design.md @@ -0,0 +1,160 @@ +# BaseOnly summary-edge filter design + +Date: 2026-07-20 + +## Goal + +BaseOnly must return the same class of applicable summaries as Tree: for a non-null caller pattern `P`, return only summaries whose stored initial access `I` overlaps `P` by containment. Tree's `filterContains` returns both stored prefixes of an exact pattern and stored descendants of an abstract pattern. Today both BaseOnly fact-to-fact (F2F) and fact-side-effect (FactSE) storage ignore `P` and broadcast every summary for the selected fact base. + +The filtering relation must be the existing AP operation, not a new approximation: + +```kotlin +BaseOnlyAccessOps.containsAccess(P, I) || BaseOnlyAccessOps.containsAccess(I, P) +``` + +This is the BaseOnly equivalent of Tree's `filterContains(P)`. The two directions matter: an abstract caller pattern selects compatible stored descendants, while a stored abstract initial selects compatible concrete callers. Exclusions do not participate in index selection; they remain attached to the returned summary and are checked by the normal edge operations. + +For `P == null`, collection remains an explicit full scan. + +## Required behavior + +For every F2F and FactSE query: + +```text +applicable(P, I) = P == null || containsAccess(P, I) || containsAccess(I, P) +``` + +In particular: + +- an exact pattern returns the exact initial and any initial prefix represented as compatible by `containsAccess`; +- an abstract static, field, or suffix slot can return all compatible descendants; +- a concrete field pattern can also match a stored `NO_ACCESSOR`, because BaseOnly field compatibility intentionally treats the missing field as compatible; +- a `NO_ACCESSOR` field in the pattern can match any stored field for the same reason; +- semantic suffix marks are compared by the existing suffix rule; +- exclusions never make an otherwise applicable initial key disappear. + +Every indexed candidate should still pass the symmetric applicability predicate before emission. That final predicate is cheap and protects correctness if index routing is later changed. + +## Storage layout + +### Shared initial-access index + +Introduce a small internal `BaseOnlyInitialAccessIndex` keyed by the three packed access slots: + +```text +static slot -> field slot -> suffix slot -> payload V +``` + +Each child table should use `ConcurrentReadSafeInt2ObjectMap`, the same single-writer/multiple-reader, eventually-consistent mechanism already proven by Tree. Payload publication follows the existing Tree approach. There are no removals. + +The index exposes: + +```kotlin +fun getOrCreate(access: BaseOnlyAccess, create: () -> V): V +fun collectAll(consume: (BaseOnlyAccess, V) -> Unit) +fun collectContainedBy(pattern: BaseOnlyAccess, consume: (BaseOnlyAccess, V) -> Unit) +``` + +`collectContainedBy` performs pattern-directed traversal. Slot routing mirrors the symmetric applicability predicate: + +- `ABSTRACT_MARK`: traverse every child at that slot; +- concrete static: traverse only the identical static child; +- concrete field: traverse the identical field and `NO_ACCESSOR` children; +- field `NO_ACCESSOR`: traverse every field child; +- suffix `ABSTRACT_MARK`: traverse every suffix child; +- concrete suffix: traverse the identical suffix child; +- suffix `NO_ACCESSOR`: only the exactly equal access can match. + +At each concrete pattern slot, the traversal also checks the stored abstract node at that slot; this is how an abstract identity such as `*` remains applicable to a concrete semantic fact. Early abstraction in the pattern can stop inspecting later slots exactly as `containsAccess` does. The final predicate check remains authoritative, so a conservative traversal may visit extra candidates but may not emit them. + +### F2F non-identity edges + +Replace `perInitial: Long2ObjectMap` with `BaseOnlyInitialAccessIndex`. A patterned lookup visits only compatible initial nodes and calls `MergingStorage.collectAll` for those nodes. + +Keep each `MergingStorage.finals` in `ConcurrentReadSafeLong2ObjectMap`: one analysis thread writes while subscriber and trace threads may read. Delta lists stay ordinary single-thread-owned collections. + +### F2F identity edges + +The existing identity storage already has a three-layer static/field/suffix trie. Add pattern-directed `collectContainedBy` operations to its layers using the same routing rules, then guard each emitted initial access with the symmetric summary-applicability predicate. + +Do not flatten identity summaries into the non-identity map: the identity trie performs exclusion intersection and subsumption while inserting, which must remain unchanged. + +### Fact-side-effect edges + +Replace `FactSESummariesBaseOnlyStorage.perInitial` with the shared initial-access index. Patterned collection visits only compatible initial nodes and emits that node's merged side effects. `SideEffectExclusionMergingStorage` already uses `ConcurrentHashMap` for the inner side-effect-kind map and needs no fact-set-related change. + +### Normalized F2F aliases + +Keep normalized aliases collection-only: + +- `trackDelta = false` for the entire normalized storage; +- never allocate or retain normalized delta lists; +- query the normalized index with the same caller pattern instead of scanning it; +- enable normalized lookup only in the existing trace-resolution phase; +- deduplicate exact `(initial access, final access, exclusion)` results across primary and normalized storage before building edges. + +Normalization can intentionally produce a different initial access and therefore a distinct trace alternative. Such alternatives must not be deduplicated merely because their final access is equal. + +## Concurrency contract + +The storage contract remains: + +```text +one writer, multiple concurrent readers, eventually consistent +``` + +Consequently: + +- shared indexes and shared final maps use concurrent-read-safe tables; +- readers may miss an insertion concurrent with their current traversal, but a later query observes it; +- readers must never use live fastutil iterators over a table that can rehash; +- no locking or snapshot copying is required; +- ordinary IFDS fact sets and delta collections remain single-threaded and should not be replaced with concurrent collections. + +This matches the confirmed Tree/Automata workload rather than strengthening the contract unnecessarily. + +## Safe implementation sequence + +1. Add a scan-and-predicate implementation first: retain the current indexes but emit only entries satisfying the symmetric applicability predicate. This is the executable correctness oracle and immediately removes unrelated summaries, although lookup remains O(number of initials). +2. Add the shared trie index and run every filter test against both implementations. +3. Switch F2F non-identity and FactSE storage to the trie. +4. Add pattern-directed identity traversal. +5. Add normalized-store filtering and exact-result deduplication. +6. Remove the scan reference only after differential and E2E verification. + +## Verification plan + +### Deterministic semantics + +Pin F2F and FactSE cases for: + +- exact pattern versus same and different initial access; +- abstract pattern versus concrete descendants; +- concrete field versus stored `NO_ACCESSOR`; +- pattern `NO_ACCESSOR` versus stored concrete field; +- concrete and abstract suffixes, including semantic marks; +- static-access equality and static abstraction; +- null pattern returning all summaries; +- exclusions changing the returned edge but not index applicability; +- identity and non-identity summaries obeying the same filter; +- normalized aliases available only when enabled, with no delta and no exact duplicate. + +### Differential oracle + +Generate random packed accesses, insert them into the scan reference and trie, and for every generated pattern compare the exact emitted key set. Compute the expected set directly with the symmetric applicability predicate. Also compare representative BaseOnly results with Tree `filterContains` after constructing equivalent APs. + +### Concurrency + +Run one writer through enough distinct slot keys and finals to force repeated rehashes while several readers issue exact, abstract, and full-scan queries. Assert no exception or malformed edge, then join the writer and assert eventual completeness. Cover primary F2F, normalized F2F, identity F2F, and FactSE storage. + +### Performance gates + +Instrument and assert structural work rather than wall-clock time: + +- initial index nodes visited; +- candidate initial keys checked; +- summaries emitted per patterned lookup; +- primary and normalized duplicates removed; +- downstream summary applications per analysis unit. + +A query with one compatible initial among many incompatible initials must emit one and should visit only the compatible trie branches. E2E acceptance should include the previously explosive Apollo, Klaw, OpenMRS, and TMS methods and require complete analyzer status before comparing findings. diff --git a/docs/baseonly-summary-edge-generalization-design.md b/docs/baseonly-summary-edge-generalization-design.md new file mode 100644 index 000000000..a251258c8 --- /dev/null +++ b/docs/baseonly-summary-edge-generalization-design.md @@ -0,0 +1,283 @@ +# BaseOnly F2F summary-edge generalization + +## Goal + +A field-sensitive method can produce a quadratic family of F2F summaries: + +```text +(-1, -2, -1) -> (-1, -2, -1) +(-1, x, -2) -> (-1, -1, -2) +(-1, x, -2) -> (-1, y, -2) +... +``` + +The end-to-end reproduction in +`BaseOnlySummaryFieldExplosionTest` nondeterministically reads 20 fields and +writes the selected value to 20 fields. Its helper retains: + +```text +1 field-abstract identity +20 concrete-field -> abstract-tail edges +20 * 19 off-diagonal concrete-field relocations +-------- +401 F2F summaries +``` + +Correct conclusion subsumption first reduces this family to: + +```text +1 field-abstract identity +20 concrete-field -> abstract-tail edges +-- +21 F2F summaries +``` + +For each fixed `x`, the edge +`(-1, x, -2) -> (-1, -1, -2)` subsumes every +`(-1, x, -2) -> (-1, y, -2)`: the premise is identical and the abstract-tail +conclusion implies every concrete-field conclusion. + +The desired bounded representation after structural-accessor generalization is: + +```text +(-1, -1, -2) /E -> (-1, -1, -2) /E +``` + +This is an explicit field-erasing widening. It is not ordinary summary-edge +subsumption. + +## Boundary between subsumption and generalization + +An F2F summary is a correlated transformation. Under exact summary semantics, + +```text +(-1, x, -2) -> (-1, -1, -2) +``` + +subsumes: + +```text +(-1, x, -2) -> (-1, y, -2) +``` + +The premise is the same and the first conclusion directionally covers the +second. This is ordinary summary-edge subsumption and must happen before +generalization. + +What subsumption does not remove is variation in the premise: + +```text +(-1, x, -2) -> (-1, -1, -2) +(-1, z, -2) -> (-1, -1, -2) +``` + +Generalization forgets that remaining `x` versus `z` distinction. It +must remain a separate operation from `BaseOnlySummaryEdgeOps.subsumes`. + +Generalization instead forgets which structural accessor was read and which +structural accessor was written. Structural accessors include ordinary fields +and the element accessor because implicit `[any]` covers both. Applying the +generalized edge produces an abstract final fact that covers every concrete +field and element accessor. False-positive paths are an accepted cost of the +widening; losing a forward result is not. + +## Field-erasure projection + +Generalization is local to one method entry, initial base, and final base. +Those bases are never merged. It is eligible only when both the initial and +final static slots are empty: + +```text +initial.staticIdx == NO_ACCESSOR +final.staticIdx == NO_ACCESSOR +``` + +An edge with any non-empty static slot is never field-generalized. Such edges +are reduced only by ordinary summary-edge subsumption. + +The eligible access shapes are: + +```text +(-1, ABSTRACT_MARK, NO_ACCESSOR) +(-1, concreteFieldOrElement, ABSTRACT_MARK) +(-1, NO_ACCESSOR, ABSTRACT_MARK) +``` + +They all project to: + +```text +eraseField(access) = (NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK) +``` + +Normal and Value suffix states, semantic marks, type-information accessors, and +final accessors must not be merged. `ANY_ACCESSOR` itself remains implicit and +is never stored in the field slot. + +An eligible edge belongs to the group: + +```text +GroupKey( + initialBase, + finalBase, + eraseField(initialAccess), + eraseField(finalAccess), +) +``` + +Its generalized representative is exactly the two projected accesses from the +group key. + +## Generalization trigger + +Do not widen a single precise relocation. Each group has a finite precision +budget: + +```text +MAX_FIELD_ENUMERATION_EDGES +``` + +Count distinct primary `(initialAccess, finalAccess)` keys remaining after +ordinary subsumption. When adding a batch would make the count exceed the +budget: + +1. remove every retained primary edge in that group; +2. mark the group permanently generalized; +3. retain its one projected representative; +4. publish the representative in the insertion delta; +5. absorb every later eligible edge in that group without re-enumerating it. + +The budget is 8: eight distinct canonical field transfers remain exact and the +ninth generalizes the group. This makes the 20-field reproduction deterministic +without aggressively widening ordinary one-off field transfers. The constant +is isolated so E2E performance/precision evaluation can tune it without +changing semantics. + +The transition is monotone for IFDS consumers. Previously emitted concrete +edges are not retracted, but all later collection observes only the generalized +representative. + +## Exclusions + +Field/element/static exclusions describe structural accessors erased by the +projection. They are therefore removed before member exclusions are combined. +Only exclusions that can occur in the remaining suffix slot are retained. + +```text +project(E) = E without field, element, static, or Any accessors +``` + +After projection, the generalized members are alternative edges with the same +premise and conclusion. Their exclusions are intersected: + +```text +E = project(E1) intersect project(E2) ... intersect project(En) +``` + +Union is incorrect here: an exclusion belonging to one erased premise would +then reject a suffix accepted by another member, producing a false negative. +A later absorbed member can only keep or shrink the representative exclusion. +If it shrinks, storage publishes the updated representative as an insertion +delta. Exact-key exclusion intersection remains unchanged before +generalization. + +## Storage organization + +Keep exact subsumption and widening as two explicit stages in `add`: + +```text +incoming edges + -> exact-key exclusion merge + -> exact correlated subsumption + -> field-erasure budget/generalization + -> immutable published snapshot and insertion delta +``` + +Required writer-owned state: + +```text +exact edge aggregates +group membership for groups below budget +set of permanently generalized group keys +published canonical summaries +``` + +Once a group is generalized, its exact aggregates and membership can be +dropped. The group key and common projected exclusion remain so later members +can update the representative without restoring accessor enumeration. + +Collection does not perform generalization. It reads the published primary +snapshot, applies the existing initial-pattern filter, and derives normalized +trace views as it does today. + +## Fact-set and trace boundary + +Summary-storage generalization does not require or enable fact-set +generalization. `summaryStorageFieldGeneralizationEnabled` controls only the +F2F summary storage. The pre-existing `fieldGeneralizationEnabled` trace view +is independent, remains disabled by default, and is not changed by this +feature. The method F2F fact set therefore remains exact in the configuration +used by summary generalization. + +The supported summary-generalization configuration keeps +`fieldGeneralizationEnabled` false. Enabling both mechanisms would give the +summary representative and the trace-only fact view different exclusion +reducers and is outside this design. + +When resolving a generalized summary, the existing BaseOnly compatibility +relation selects its concrete method-side witnesses. Each selected witness +must connect an exact member premise and conclusion covered by the generalized +edge. The end-to-end regression test must resolve a complete trace with +summary generalization enabled and fact/trace generalization disabled. + +## Required tests + +### End-to-end reproduction + +- The 20-field sample finds the vulnerability. +- Before the corrected subsumption it demonstrates the 401-edge family. +- Correct conclusion subsumption reduces it to 21 retained edges. +- After generalization the same method/base pair collects one field-erased + summary and still finds the vulnerability. +- Tree remains the precision oracle; every Tree finding remains reachable in + BaseOnly. + +### Storage laws + +- below-budget groups remain precise; +- crossing the budget replaces the group with one representative; +- all insertion orders produce the same final representation; +- a batch crossing the budget emits only the representative from that batch; +- later members of a generalized group do not re-expand it; +- a later member that shrinks the common suffix exclusion publishes the + broader representative; +- different initial/final bases do not share a budget; +- any edge with a non-empty initial or final static slot is never generalized; +- static-prefixed edges continue to use ordinary subsumption; +- Normal/Value and semantic/type/final suffixes do not merge; +- the representative intersects suffix-valid contributor exclusions; +- structural exclusions erased by projection are not retained; +- element-accessor members participate in the same budget as field members; +- a later absorbed member updates and re-emits the representative only when + its common suffix exclusion shrinks; +- unrelated summaries remain unchanged; +- normalized aliases remain collection-only. +- summary-storage and fact-trace generalization flags are independent in both + directions. + +### Consumer laws + +- applying the generalized edge through the real delta/concat/exclusion + refinement path covers every forward result produced by each removed + contributor; +- initial-pattern filtering returns the generalized edge for every compatible + concrete field caller; +- full trace resolution succeeds through the generalized method-side witness; +- source-to-sink reachability survives after all concrete contributors have + been discarded. + +### Performance gate + +Instrument retained primary summaries and summary applications. The 20-field +sample must progress from 401 current members to 21 after corrected +subsumption, then to one generalized member. Repeated calls must not recreate +the concrete matrix. diff --git a/docs/baseonly-summary-edge-subsumption-design.md b/docs/baseonly-summary-edge-subsumption-design.md new file mode 100644 index 000000000..c7239a48b --- /dev/null +++ b/docs/baseonly-summary-edge-subsumption-design.md @@ -0,0 +1,271 @@ +# BaseOnly F2F summary-edge subsumption + +## Goal + +`MethodInitialToFinalBaseOnlyApSummariesStorage` has two operations: + +- `add` merges new summaries into a minimal covering set and reports its + insertion delta; +- `collect` returns the retained summaries matching an optional initial-fact + pattern, including normalized read-only views when enabled. + +The storage does not expose separate forward and backward modes. Edge +subsumption is an internal decision made by `add`. Its predicate must preserve +the correlated transformation used by every consumer of a collected summary. + +For example: + +```text +this.a.* /{} -> this.b.* /{} +this.a.MARK /{} -> this.b.MARK /{} +``` + +The second edge is redundant. Applying the first edge to `this.a.MARK` extracts +the residual `MARK` and grafts that same residual onto `this.b.*`, producing +`this.b.MARK`. Backward resolution performs the inverse operation and rebuilds +`this.a.MARK`. + +Subsumption is pairwise. The first implementation does not need to prove that a +union of several existing edges covers a new edge. + +## Semantic relation + +Let an edge be: + +```text +E = (initial, final, exclusions) +``` + +Define: + +```text +subsumes(cover, covered) +``` + +as directional inclusion of the summary transformations, not independent +containment of the two access paths. + +### Residual correlation + +It is incorrect to use only: + +```text +covers(cover.initial, covered.initial) && +covers(cover.final, covered.final) +``` + +The initial and final sides of an F2F edge are correlated by one residual. For +example, `a.* -> b.*` covers `a.M -> b.M`, but it does not cover +`a.M -> b.N`. + +The authoritative predicate must therefore: + +1. handle equal premises by directional conclusion coverage: if the initial + facts are equal, `cover` subsumes `covered` when `cover.final` contains + `covered.final`, subject to the exclusion rules below; +2. otherwise apply `cover` to `covered.initial` using the same residual operation as + `FinalFactAp.delta`; +3. graft each surviving residual onto `cover.final` using the same operation as + `FinalFactAp.concat`; +4. accept only if one produced final fact, including its effective exclusions, + is exactly `covered.final / covered.exclusions`; +5. verify that backward `InitialFactAp.splitDelta` on `covered.final` and + `cover.final` can recover the same residual and that concatenating it with + `cover.initial` exactly reconstructs `covered.initial`. + +The equal-premise rule is implication between two conclusions, not residual +grafting. For example: + +```text +(-1, x, -2) -> (-1, -1, -2) +``` + +subsumes: + +```text +(-1, x, -2) -> (-1, y, -2) +``` + +because `(-1, -1, -2)` contains `(-1, y, -2)`. Requiring exact final equality +in this case incorrectly retains every enumerated `y`. + +In notation, for at least one residual `d`: + +```text +d in residual(covered.initial, cover.initial, cover.exclusions) +apply(cover, covered.initial / covered.exclusions) + == covered.final / covered.exclusions + +d in splitResidual(covered.final, cover.final, cover.exclusions) +concat(cover.initial, d) == covered.initial +``` + +The two witnesses must denote the same logical residual. Checking the forward +and backward conditions independently without correlating their residuals can +accept an edge that works in analysis but cannot resolve a trace. + +This should be implemented once as: + +```kotlin +BaseOnlySummaryEdgeOps.subsumes(cover, covered): Boolean +``` + +The implementation should use shared access-level residual, exclusion, graft, +and concat operations. It must not duplicate AP-slot case logic in the storage. + +Directional BaseOnly coverage is not sufficient here. In particular, implicit +`AnyAccessor` makes `.*` cover `.f.*`, but the transformations `.* -> .*` and +`.* -> .f.*` are not trace-equivalent: the latter installs the residual under +`f`. Deleting it loses the backward field-installation step. Exact correlated +reconstruction is intentionally conservative; a retained redundant edge costs +space, while a falsely deleted edge loses trace behavior. + +### Empty residual and exclusions + +If `cover.initial == covered.initial`, both edges apply with an empty residual. +The final fact produced by `cover` has: + +```text +effective exclusions = + covered.exclusions union cover.exclusions +``` + +That produced final fact must equal +`covered.final / covered.exclusions`. Since the final accesses must be equal, +the exclusion check reduces to: + +```text +covered.exclusions.contains(cover.exclusions) +``` + +Exclusions are intersected only for repeated occurrences of the same exact +`(initial, final)` edge. Different finals retain independent exclusions. + +If a representation forces several distinct final edges into one record, their +exclusions must instead be combined by union. BaseOnly stores the finals +separately, so this lossy fallback is unnecessary. + +For a non-empty residual, `cover.exclusions` is checked by the ordinary +residual operation. If it rejects the residual, `cover` does not subsume the +edge. If it accepts the residual, its exclusions are not copied to the mapped +fact by summary application, so a separate whole-set subset check would be +unnecessarily restrictive. + +Normalized initial aliases are collection-only trace views. They do not +participate in primary-edge subsumption. + +## Add protocol + +The storage writer handles one `add` batch as follows: + +1. Reject edges containing a collapsed access. +2. Intersect every incoming exclusion into the aggregate for its exact + `(initial, final)` key. +3. Rebuild the retained and incoming records for the affected exact keys. +4. Combine those rebuilt candidates with summaries for unaffected keys and + retain a deterministic antichain using the authoritative `subsumes` + predicate. +5. Publish the complete new snapshot and append every newly visible primary + summary to the insertion delta. + +Steps 3 and 4 use an index only to obtain a conservative candidate set. The +authoritative predicate is always evaluated before rejection or removal. + +If two different representations mutually subsume one another, choose a stable +winner with a deterministic canonical key order. This makes the final +antichain independent of insertion order. + +Delta collection occurs after the whole input batch, as it does today. Thus an +edge inserted and then subsumed by a later edge in the same batch emits no +delta. Removing an edge published by an earlier call emits no retraction: the +new edge covers its behavior, and IFDS propagation remains monotone. + +## Collect protocol + +`collect` reads one immutable snapshot of the retained primary summaries: + +1. Add each primary summary that matches the optional initial-fact pattern. +2. When normalized views are enabled, derive the normalized initial from each + primary and add it if it matches the pattern. +3. Merge duplicate `(initial, final)` views by exclusion intersection. +4. Materialize the resulting summary builders. + +Normalized views have no independent storage state and never contribute an +insertion delta. + +## Storage representation + +The logic-first implementation keeps: + +- writer-owned merged exclusions keyed by exact `(initial, final)` edge; +- one volatile immutable list of retained primary summaries. + +`add` computes and publishes a complete replacement list. `collect` reads only +that published list. Identity and non-identity edges share the same +representation and the same subsumption authority. + +The concurrency contract remains: + +```text +one writer, multiple eventually-consistent readers +``` + +Snapshot publication must be visible to readers. A reader may observe the +complete old snapshot or the complete new snapshot during an insertion, but +never a partially rebuilt set. After the writer completes, later readers +observe the new antichain. + +## Required tests + +### Core examples + +- `a.* -> b.*` subsumes `a.M -> b.M`, in both insertion orders. +- `a.* -> .*` subsumes `a.* -> b.*`: the premise is identical and the first + conclusion contains the second. +- `(-1, x, -2) -> (-1, -1, -2)` subsumes + `(-1, x, -2) -> (-1, y, -2)`. +- `a.* -> b.*` does not subsume `a.M -> b.N`. +- `a.* -> b.M` does not subsume `a.N -> b.M` when graft cannot preserve the + residual. +- `Normal` and `Value` terminal modes remain distinct. +- identity/non-identity cross-storage candidates are checked. + +### Exclusions + +- an abstract edge excluding `M` does not subsume the concrete `M` edge; +- for equal initials, `{}` subsumes `{M}`, but `{M}` does not subsume `{}`; +- exact-key exclusion updates rerun eviction without changing unrelated + finals; +- an exclusion unrelated to a non-empty residual does not by itself prevent + subsumption. + +### Storage and delta + +- a batch containing narrow then broad emits only the broad delta; +- a previously published narrow edge disappears from later collection after a + broad edge is added; +- all permutations produce the same canonical antichain; +- patterned and full collection never return tombstoned records; +- normalized views are derived only from active primary records. + +### Consumer differential + +For a bounded set of Tree-equivalent accesses and caller extensions: + +1. apply both edges and record all forward outputs; +2. remove the edge classified as covered and repeat; +3. assert that every previous output is directionally covered; +4. resolve backward from every output and assert that every previous entry + precondition is directionally covered. + +Also test reflexivity and transitivity of the predicate on generated canonical +edges. A transitivity counterexample is a release blocker because permanent +tombstones rely on a chain of newer covering edges continuing to cover every +older removed edge. + +### Concurrent readers + +Force map rehashes while one writer repeatedly replaces narrow edges with broad +ones and several readers perform full and patterned collection. Assert no +exception or malformed edge, then join the writer and assert eventual +antichain completeness. diff --git a/docs/baseonly-tree-conformance.md b/docs/baseonly-tree-conformance.md new file mode 100644 index 000000000..c10476eaa --- /dev/null +++ b/docs/baseonly-tree-conformance.md @@ -0,0 +1,281 @@ +# BaseOnly conformance to Tree + +Status: **normative** for the BaseOnly release mitigation. + +This document defines how every BaseOnly access-domain operation is compared +with Tree. The domain itself is defined in +[`baseonly-access-domain-spec.md`](baseonly-access-domain-spec.md). + +## 1. Comparison model + +The release target is a small, independent logical-graph reference model that +does not call BaseOnly production operations to compute expected values. The +current differential suite is bounded and uses Tree values plus observable +BaseOnly reads; it is useful regression evidence, but it is not yet that +independent projector. The operation ledger records this open evidence gate. + +For a Tree value `T`, `project(T)` is the minimal canonical BaseOnly antichain +whose union covers every Tree path. A Tree may project to multiple BaseOnly +values when its branches have incompatible static or semantic terminals; forcing +those branches into one packed value is not permitted to lose either branch. +The test-reference `canonicalJoin` therefore returns a fact set. It may return one widened access +for an ordinary retained-component difference, but it returns two accesses when +the only difference is value-accessor state. + +Results are compared by denotation, not packed equality: + +```text +treeCovered(treeResults, baseResults) := + ⋃ Paths(treeResults) ⊆ ⋃ Paths(baseResults) +``` + +The default differential assertion is `treeCovered`. Exact equality is required +only where the table below says **exact**. BaseOnly-only paths are permitted only +when they follow from the documented projection/widening rule. + +Every differential fixture uses the same: + +- base and exclusions; +- accessor identities; +- field-sensitivity mode; +- `AnyAccessorUnrollStrategy`; +- `FactTypeChecker` outcome. + +Base mismatch and type/exclusion rejection are tested independently so an +intentional primitive drop cannot hide access-path loss. + +## 2. Tree behavior that BaseOnly inherits + +The following Tree behaviors are interface contracts: + +- `AccessTree.getStartAccessors()` returns the root edge labels and therefore + includes `AnyAccessor` when the root has an Any edge. +- `AccessTree.getAllAccessors()` calls `collectAccessorsTo`, which deliberately + ignores `AnyAccessor` while recursively collecting concrete accessors and `$`. +- Tree `startsWithAccessor` and `readAccessor` query the same logical edge; + successful start implies a non-null read. +- Tree initial access paths are linear; Tree final access trees may branch. +- Tree final `delta` checks the base, consumes the initial path, applies initial + exclusions to the remainder, and may return both empty and nonempty deltas. +- Tree final concat grafts at abstract leaves and applies the supplied type + checker. +- Tree initial `splitDelta` finds a matched prefix and a remainder; concat + reconstructs it. +- Tree `clearAccessor` subtracts a root branch rather than reading/promoting it. +- Tree filters operate branch-wise on the logical tree. +- Tree rebase changes only the base. +- Tree exact equality is not containment and is not overlap. + +BaseOnly never stores an Any field slot. An explicit or forgotten Tree +structural edge projects to the implicit structural self-loop of a semantic or +suffix-abstract state. The accessor views remain asymmetric: start accessors +expose Any, while all accessors do not. + +Tree distinguishes the semantic paths `M $` and `T $` from paths having a +category wrapper, `V M $` and `G T $`. BaseOnly preserves that distinction with +the value-accessor state: + +```text +Normal = the normal suffix path +Value = the value suffix path through ValueAccessor +``` + +A Tree union containing both paths projects to two BaseOnly facts. There is no +packed state representing their union. + +## 3. Operation conformance matrix + +“Projected Tree result” below means the independent `project` operation from the +reference model. + +| Operation | Tree contract | Permitted BaseOnly widening | Forbidden BaseOnly behavior | Differential property | +|---|---|---|---|---| +| codec pack/unpack | Tree has no packed equivalent | none; codec is representation-only | accept invalid category/order/range or change logical state | logical state before/after codec is exact | +| validate | Tree values are structurally valid | none | allow a packed state with no Tree-relative denotation | every accepted state builds the reference graph; every generated invalid state is rejected | +| `project`/`canonicalize` | preserves the Tree graph | discard later structural precision per the outermost rule | omit a Tree path or replace the outermost structural with an inner one | `Paths(T) ⊆ Paths(project(T))`; idempotent | +| `build` | repeated Tree construction in sequence order | canonical projection only | reorder malformed paths, conflate `Normal` with `Value`, silently lose a path | ordinary input projects to `Normal`; a `ValueAccessor`-prefixed taint mark projects to `Value`; malformed wrapper pairs are rejected | +| `abstractAt` | construct prefix ending at abstract node | canonical prefix projection | unchecked position; retain components after abstract node | exact projected Tree graph | +| `prependAccessor` | `AccessNode.addParent` / linear `AccessNode` parent | field truncation to an absent slot with implicit Any | replace an outer field with an inner field; return less than Tree | projected Tree prepend is covered | +| `consume` / `readAccessor` | `getChild` (final) or exact head read (initial) | universal reads enabled by implicit Any and by a retained field's projected structural tail | fail a Tree-successful read; allow a `Normal` terminal to read `ValueAccessor` or a `Value` root to skip it | every Tree read result projects into BaseOnly read results; reading `ValueAccessor` returns a `Normal` residual | +| `startsWithAccessor` | Tree edge membership (`contains` for final; exact head for initial) | true for an implicit-Any-covered structural read | false when corresponding BaseOnly read succeeds, or true with null read | exact agreement with BaseOnly `consume`; Tree true implies BaseOnly true after projection | +| `getStartAccessors` | root edge labels, including Any | implicit Any only | omit Tree/projected Any; enumerate every possible concrete field instead of Any | `Normal -> {Any,X}`, `Value -> {Any,W(X)}` after the common prefix | +| `getAllAccessors` | recursive concrete collection; **Any excluded** | concrete accessors retained by logical expansion | include Any; omit the wrapper for `Value`; invent one for `Normal`; omit semantic/final | exact set of projected logical concrete labels for each state | +| head/first | first logical concrete/edge accessor | absence when only virtual Any/abstract remains | expose type before group for `Value`, or group before type for `Normal` | exact projected logical view for each fact; collections iterate each fact | +| `size` | final Tree `countNodes`; initial Tree linear node count | BaseOnly intentionally uses a different bounded retention metric | exceed three or count virtual/wrapper nodes inconsistently | exact occupied concrete-slot count in `[0,3]` | +| `depth` | final Tree `maxDepth`; initial Tree path length | BaseOnly intentionally aliases its bounded packed size and omits Tree's Any-cycle sentinel | use it as a semantic path length | exact equality with BaseOnly packed size | +| `isAbstract` | current logical node has abstract acceptance | none beyond projected abstraction | report a later abstraction before its concrete prefix is consumed; call every empty delta concrete | exact against projected current node | +| `clearAccessor` | remove matching root branch | least canonical cover of surviving branches; an implicit Any continuation can require retaining the compact state | remove an unrelated surviving branch | every projected Tree survivor is covered; 39 mutation traces pin the root-terminal case | +| exact equality | equal logical initial/final shape under Tree's method | none | use overlap/compatibility; ignore base at fact level | exact on projected canonical graph/base/exclusions as applicable | +| access `covers` | Tree final `AccessNode.contains` intent, generalized for canonical storage keys | projected directional language inclusion | symmetric missing-field compatibility; claim `Normal` covers `Value` or vice versa | Tree containment true implies BaseOnly coverage; state equality and coverage laws hold | +| final fact `contains(initial)` | Tree `AccessNode.contains` after equal base check; exclusions ignored | projection-aware missing-structural compatibility; one manager is assumed | cross-base true; using this symmetric relation as storage subsumption | every Tree-true pair remains true after projection; split-delta is aligned with the same projected match | +| initial fact `contains(initial)` | Tree `AccessPath.contains` is exact fact equality | zero-residual access-prefix match after lossy canonical projection; base remains exact and path-local exclusions are ignored; one manager is assumed | use arbitrary overlap or nonempty residual; cross-base match | every projected Tree-equal pair matches; widening is limited to zero-residual access and exclusion erasure | +| `mayOverlap` | candidate relation inferred from nonempty Tree intersection | false positives allowed in index only | false negative candidate; use as final containment | every Tree-overlapping pair is a candidate; symmetry law | +| `delta` (final) | consume initial path, check `$`, filter remainder exclusions; empty/nonempty branches | project residual trees, possibly returning multiple facts | skip base check; change value-accessor state; drop one fact because another state shares its suffix | every Tree delta is covered with state reflecting the unmatched path | +| final `concat` | `concatToLeafAbstractNodes(typeChecker, delta)` | canonical projection after graft | ignore checker; recreate primitive/incompatible path; change value-accessor state without a different input fact | every Tree concat result is covered; the terminal-contributing operand's state is preserved | +| `splitDelta` (initial) | match against final tree, filter remainder, return matched prefix + delta | projected matched prefix/residual | AP-slot special case that loses reconstructability; lose wrapper position or value-accessor state; ignore base/exclusion | every Tree pair has a covering BaseOnly pair and concat covers original initial | +| initial/delta `concat` | linear node concat | canonical projection | non-associative result after canonicalization; change value-accessor state; cross-kind slot rejection not made by Tree | projected Tree concat is covered; identity/associativity and state-preservation laws | +| exclusions | Tree filters exact outgoing logical branches | projection of surviving branches; implicit Any subtraction may retain the compact cover | treat `Universe` as Empty; drop a surviving branch; mishandle group/type | projected Tree filtered graph is covered | +| compatibility filter | Tree checks exactly an edge whose child has direct abstract acceptance; ancestors and wholly concrete paths bypass the checker | compatibility cover when paths merged | check every path edge or every ancestor of an abstract leaf; omit the direct predecessor | every Tree concrete path and every Tree-compatible abstract path survives projected filter | +| final fact filter | Tree `filterAccessNode`, branch-wise | retain a sound projection per fact | reuse filter state across facts or change their value-accessor states | evaluate each complete path independently and return exactly the surviving facts | +| `abstractOnly` | Tree abstract root with same base/exclusions | BaseOnly currently preserves an existing static/field abstraction position | silently collapse distinct positions before a root representation is specified | restored slot-preserving behavior is pinned; Tree-root equivalence remains open | +| `removeAbstraction` | remove abstract acceptance, keep concrete branches; null if empty | later-AP widening or an explicit transient collapsed suffix until rebase | persist the transient marker; lose a concrete prefix | projected Tree survivors remain covered through the flow-function lifecycle | +| `rebase` | base substitution; in the remove/rebase flow lifecycle it restores suppressed abstract acceptance | transient collapsed restoration only | alter any stable access or exclusions | stable access/exclusions exact; transient access restored; base replaced | +| `exclude` / `replaceExclusions` | exclusion-set update only | none | alter access/base | access/base exact; exclusion algebra exact | +| most-abstract factories | Tree null initial / abstract-root final | their canonical projections | choose a state that does not cover reference | exact projected logical graph | +| final factories | Tree exact `$` initial/final | none | manufacture empty/open/abstract state | exact projected `$` graph | +| initial-fact abstraction | Tree refinement ladder, Any unroll, type checks | deduplicate/merge projected pairs | ignore checker/unroll, omit Tree pair, emit mixed identity | every Tree pair is covered; no mixed concrete/abstract identity | +| render | Tree printer exposes graph distinctions | compact notation | hide AP position, virtual Any, or value-accessor state so distinct states print identically | generated canonical states have unambiguous renderings | +| serialize | Tree serializer round-trips its logical value | compact BaseOnly payload | lose value-accessor state; truncate a suffix outside the 23-bit range; add a BaseOnly magic/header/version | every canonical value round-trips exactly | + +## 4. Required combined scenarios + +Single-operation tests are insufficient. The differential suite must include +these compositions because storage and trace resolution consume them as units. + +### 4.1 Prepend, start, read + +For every generated Tree value `T` and accessor `a` for which prepend succeeds: + +```text +B = project(prependTree(T, a)) +assert a in getStartAccessors(B) or a is represented through a documented Any edge +assert startsWith(B, a) +assert read(B, a) covers project(T) +``` + +Include two distinct fields and verify the first/outermost field remains the +retained concrete field. + +### 4.2 Delta and concat + +For every Tree final `F`, initial `I`, and Tree delta `D ∈ F.delta(I)`: + +```text +BD ∈ projectDelta(D) +BF ∈ project(F) +BI ∈ project(I) +concat(BI, BD) covers the matched initial reconstruction +concat(BF-prefix, BD, checker) covers Tree concat when non-null +``` + +Include identity plus nonempty residual, abstraction at each position, a field +followed by a semantic mark, type group/type, exclusions, and primitive rejection. + +### 4.3 Split-delta and concat + +For every pair returned by Tree `I.splitDelta(P)`: + +```text +(matched, delta) = projected BaseOnly pair +concat(matched, delta) covers project(I) +``` + +Exercise `Tree initial = f.g.M.$` against patterns abstract at root, after `f`, +and before `M`. This scenario forbids special-case slot alignment that cannot +reconstruct the original path. + +### 4.4 Clear, start, all-accessors + +For every root accessor `a`: + +- if Tree clear removes the only branch, BaseOnly returns null; +- otherwise every Tree survivor is covered; +- `a` is absent from the resulting start set when exactly removable; +- an Any root edge is present in start accessors before clear but absent from all + accessors both before and after clear. + +### 4.5 Filter and concat + +Graft a delta that contains one compatible reference branch and one incompatible +or primitive branch. BaseOnly keeps a cover of the compatible Tree branch and +does not recreate the rejected branch as an exact fact. + +### 4.6 Serialize and operate + +Round-trip every canonical operand, then repeat prepend/read, delta/concat, +clear, coverage, and filtering. Results before and after serialization are exact +canonical equals. + +### 4.7 Rebase and abstraction lifecycle + +For each abstraction position: + +```text +!A.isCollapsed implies rebase(A).access == A.access +removeAbstraction(abstractOnly(A)).rebase(A.base) == abstractOnly(A) +``` + +For generated projected trees with both abstract and concrete branches, +`removeAbstraction` retains a cover of every concrete Tree branch. Because the +compact representation cannot encode a concrete prefix with no accepting +terminal, it may move abstraction to the suffix or return the transient +collapsed state. `rebase` completes that lifecycle; no storage +or serializer accepts the transient state. + +### 4.8 Compact value-accessor state + +Run the same operation chain for `M $`, `V M $`, `T $`, `G T $`, and for the +two-fact unions of each pair: + +```text +build -> getStart/getAll -> read -> clear -> exclude -> filter + -> delta/splitDelta -> concat -> serialize -> repeat +``` + +For each semantic accessor `X`, assert: + +- exact construction yields `Normal` for `X $` and `Value` for `W(X) X $`; +- joining the two yields two facts, independent of insertion order; +- reading `W(X)` from `Value` yields `Normal`; +- clearing or excluding a zero-length terminal root retains a sound compact + cover when the same terminal survives behind implicit Any; +- residual and concat preserve the surviving state; +- serialization preserves both states exactly without a BaseOnly header. + +## 5. Differential generator + +The bounded generator must include: + +- two bases; +- two statics; +- two fields plus element; +- Any with an unroll strategy that both accepts and rejects selected fields; BaseOnly's + implicit universal Any must remain a superset in both cases; +- two taint marks, Value, Final; +- TypeInfoGroup plus two types; +- `Normal` and `Value` states, plus their two-fact union, for every + generated taint mark and type; +- Tree paths to depth at least five; +- Tree branching at root and below a field; +- abstract acceptance at root and internal nodes; +- empty, concrete, and Universe exclusions where the API permits them; +- field-sensitive and field-insensitive BaseOnly managers; +- always-compatible and selectively-incompatible type checkers. + +Generate valid Tree values directly. Generate invalid BaseOnly codec states +separately; do not use invalid values as differential operands. + +For each operation, compare the union of result denotations. When BaseOnly emits +extra paths, assert that each extra follows from one named widening rule: + +1. later structural truncation; +2. field-insensitive structural erasure; +3. implicit structural Any before a semantic or suffix-abstract state; +4. branch join into a canonical cover; +5. separate facts retained when joining `Normal` and `Value` states. + +No catch-all “BaseOnly is approximate” waiver is permitted. + +## 6. Release verdict for an operation + +An operation is conformant only when all are true: + +1. its production code delegates to the shared primitive named in the access + spec; +2. its algebraic laws pass bounded exhaustive tests; +3. its differential property and relevant combined scenarios pass against Tree; +4. all BaseOnly-only results are attributed to a named widening rule; +5. no retained dataflow regression contradicts the result. + +Until then its release verdict is not “perfect design and implementation,” even +if example and golden tests pass. diff --git a/docs/conductor-full-trace-mitigation-plan-2026-07-28.md b/docs/conductor-full-trace-mitigation-plan-2026-07-28.md new file mode 100644 index 000000000..8afeb0581 --- /dev/null +++ b/docs/conductor-full-trace-mitigation-plan-2026-07-28.md @@ -0,0 +1,299 @@ +# Conductor BaseOnly trace-resolution mitigation plan + +## Verdict + +The Conductor timeout is caused by **eager multiplication of observable action +alternatives with backward continuation states** in `MethodTraceResolver`. + +The resolver does not visit one `TraceEntry` repeatedly. Instead, it constructs +millions of distinct call-summary/action alternatives which project to a much +smaller set of `(statement, edges)` continuations. It then repeats the same +backward transfer work for every alternative. + +This is not primarily a summary-storage lookup, virtual-call lookup, or one +exceptionally large Cartesian-product problem. Those operations are visible in +profiles because they are repeated under the multiplied state space. + +## Phase boundary + +`TaintAnalyzer#resolveActionableRules` first calls +`resolveVulnerabilityInterProceduralTraces(resolveAllTraces = true)` and only +then calls `resolveVulnerabilityActionableRules`. + +On the reference run: + +- prescan: about 24.2 seconds; +- shallow forward scan: about 31.6 seconds; +- start-to-final/inter-procedural trace resolution: remained at 2/19 items for + about 61 seconds and timed out; +- actionable-entry search did not become the active workload before timeout + cleanup. + +Therefore the current bottleneck precedes `TraceActionSearcher` and full action +rule evaluation. + +## Concrete evidence + +The instrumented Conductor run processed 5,784 trace builders and recorded: + +| quantity | count | +|---|---:| +| raw call choices | 104,330 | +| merged call/target alternatives | 1,705,232 | +| resolved call-summary alternatives | 8,602,938 | +| selected summary alternatives | 2,510,609 | +| emitted predecessors | 8,602,938 | +| distinct `(statement, edges)` continuations | 115,295 | + +The resolved alternatives therefore contain a **74.6× continuation +multiplicity**. Selected summaries alone contain a **21.8× multiplicity**. + +A representative hot builder had: + +```text +edges=20 +rawChoices=17 +merged=287 +resolved=1451 +selectedSummaries=420 +hotPredecessors=1451 +emittedContinuationKeys=20 +``` + +Another had 25 facts, 481 merged alternatives, 3,282 resolved alternatives, +1,365 selected summaries, and only 26 continuation keys. + +One concrete call is: + +```text +%111 = %10.scheduleNextIteration(%12, %11, %13) +``` + +BaseOnly wildcard facts such as `var(31).*/{}`, `var(30).*/{}`, and +`var(85).*/{}` match summaries from seven exit statements. For example: + +- `var(31).*/{}`: 124 resolved summaries, 19 selected; +- `var(30).*/{}`: 80 resolved summaries, 19 selected; +- `var(85).*/{}`: 22 resolved summaries, 8 selected. + +Marked field variants match still more exact summary alternatives while many of +them produce the same predecessor edge set. + +Thread dumps during the timeout show all workers allocating or comparing these +states in: + +- `MethodTraceResolver#mergeCallActions`; +- `MethodTraceResolver#resolveCallPassSummary`; +- `MethodTraceResolver#selectWeakestEntries`; +- `MethodTraceResolver#containsEntryEdge`; +- `MethodTraceResolver.EntryManager#entryId`; +- `JIRCallResolver` target/context resolution. + +This distributed profile is consistent with multiplicative state construction: +no single operation owns the entire cost. + +## Incorrect representation boundary + +The exact observable action identity is: + +```text +(statement, unchanged edges, primary action, other actions) +``` + +Different alternatives must remain correlated because their nested summary, +rules, unchanged edges, and validity may differ. + +The backward transfer identity is only: + +```text +ContinuationKey(statement, predecessor edges) +``` + +`MethodTraceResolver#mergeCallActionsCombinations`, +`MethodTraceResolver#resolveCallSummary`, and +`MethodTraceResolver#addPredecessorActions` currently enumerate action +alternatives first and immediately materialize their predecessor entries. This +lets action provenance multiply the reachability state even though backward +transfer depends only on `ContinuationKey`. + +The correct separation is: + +```text +exact action alternatives --many-to-one--> continuation +continuation --computed once--> predecessor continuations +``` + +The public full trace must still contain every relevant `TraceEntry.Action`. +Only the internal transfer computation is shared. + +## Rejected local mitigations + +The following prototypes all retained the 2/19 timeout: + +1. globally canonicalizing action entries by `(statement, edges)`; +2. partitioning call-summary products by common exit before merging; +3. a start-only continuation dynamic program inside summary resolution; +4. per-resolver caches for call targets and resolved call summaries. + +The first prototype is also not generally safe: globally merging observable +action nodes can mix their successor incidence. The third acted too late and +could not avoid construction in the surrounding call/action pipeline. + +These experiments rule out a late deduplication or cache-only mitigation. + +## Proposed representation + +### 1. Intern edge sets and continuations + +Introduce internal identifiers: + +```kotlin +@JvmInline +value class EdgeSetId(val value: Int) + +data class ContinuationKey( + val statement: CommonInst, + val edges: EdgeSetId, +) +``` + +`TraceBuilder` processes each `ContinuationKey` once. It records all observable +action emissions attached to that continuation, but does not enqueue each +action as an independent transfer state. + +### 2. Preserve exact action alternatives + +Keep the public representation: + +```kotlin +TraceEntry.Action(statement, edges, actionId) +FullStart2FinalTrace.actionVariants[actionId] +``` + +For each successor entry, group exact variants only by the continuation edge +set. Do not merge action nodes belonging to different successor incidences. + +Internally record: + +```kotlin +data class PendingActionEmission( + val successorId: Int, + val continuation: ContinuationKey, + val variants: Set, +) +``` + +After reachability is known, materialize the public graph as: + +```text +predecessor -> Action(variants) -> successor +``` + +Internal continuation nodes must not appear in `FullStart2FinalTrace`. + +### 3. Build call actions as a symbolic choice DAG + +Replace eager Cartesian-product lists with a layered family: + +```kotlin +data class ChoiceNodeKey( + val layer: Int, + val mode: PropagationMode, + val continuationEdges: EdgeSetId, +) + +enum class PropagationMode { + Neutral, + SourceOnly, + NonSource, +} +``` + +Each transition retains the exact selected rule/summary action. Nodes with the +same layer, mode, and accumulated continuation share the remaining suffix +computation. + +Apply this to: + +- call-edge combinations; +- dynamic callee/entry-point choices; +- call-summary choices; +- rule-action choices; +- sequential action combinations. + +Summary alternatives may be normalized by +`(callee, exit statement, summary edges, final edges)`. Alternatives from +different exit statements must never be merged. + +### 4. Materialize only reachable family paths + +For start-to-final resolution, traverse continuation reachability without +materializing action payloads. + +For full resolution: + +1. determine reachable continuation/family nodes; +2. enumerate exact variants only for reachable family paths; +3. assign `actionId`s and populate `actionVariants`; +4. insert observable action entries between their shared predecessors and exact + successors; +5. remove all internal continuation/family nodes. + +There must be no bypass edge around an action. Otherwise invalid nested-summary +filtering could incorrectly preserve a path. + +## Correctness tests + +Before enabling the new representation, compare it with exhaustive resolution +on bounded samples: + +1. exact set of `ActionVariant` values; +2. exact start entries and final entry; +3. exact public adjacency after internal-node removal; +4. source-only, pass-only, mixed source/pass, and unresolved-call cases; +5. variants with identical continuations but different rules or nested + summaries; +6. invalid nested summary in only one variant; +7. multiple callees and multiple exit statements; +8. ND facts and cyclic control flow; +9. sequential action combinations; +10. no internal node in `FullStart2FinalTrace`. + +The current BaseOnly trace-entry explosion sample should additionally assert +that equivalent continuations are processed once while all action variants +remain present. + +## Performance acceptance + +Add per-phase counters: + +```text +raw alternatives +symbolic choice nodes +continuation keys processed +reachable action variants materialized +public trace entries +peak resolver memory +``` + +The Conductor acceptance criteria are: + +1. all 19 actionable-rule traces resolve within the existing timeout; +2. actionable rules and findings match the exhaustive implementation; +3. continuation processing is close to the measured 115,295-key quotient, not + the 8.6-million resolved-alternative count; +4. full resolution materializes every reachable action variant required by the + API; +5. core and both query-language suites remain green. + +## Experimental validation of the plan + +The probe validates the plan's central quotient: 8,602,938 exact alternatives +map to 115,295 continuation keys, so sharing backward transfer at that boundary +removes the measured 74.6× redundant dimension without deleting action +semantics. + +The rejected prototypes validate the required placement: deduplication after +action construction and cache-only changes do not affect the timeout. The +sharing must therefore happen before eager action/summary materialization and +must be carried through the full-trace representation. diff --git a/docs/e2e-regression-2026-08-19.md b/docs/e2e-regression-2026-08-19.md new file mode 100644 index 000000000..42db1bd38 --- /dev/null +++ b/docs/e2e-regression-2026-08-19.md @@ -0,0 +1,353 @@ +# e2e regression analysis — `saloed/base-only-clean` vs base + +Run of 2026-08-19. New analyzer `e2bd0f883` (the then branch tip, 21 commits) against base +`6adc217f2`, 28 projects. Raw output in `run-debug/`. + +**Status.** This analysis is why the branch was rewritten. The two optimisations it convicts — +shallow-scan statement collapsing and class-static call skipping — were removed rather than +repaired, so the history this document analyses no longer exists and the commits it cites by hash +are not on the branch. Sections 1-8 are kept verbatim as the evidence for that decision; section 9 +records what was actually done. + +**Verdict: the branch is not ready.** 23 findings are genuinely lost (2 more are the accepted +boxed-primitive class and are dismissed), and thingsboard's shallow scan is aborted by the memory +watchdog. + +Every loss now has an identified source, and the headline is that **none of them is a precision +limit of field-insensitivity.** Three of the branch's own performance commits prune or skip work in +an unsound direction, and the staged pipeline then converts each of those into a hard false negative +because the full scan can only look where the shallow pass already looked. + +--- + +## 1. How much was actually lost + +`run-debug/regression-diff/report.md` reports 12 failing projects. That number is +inflated: the diff keys on the `codeFlows` **count**, so a finding whose flow count +changed at an unchanged location is counted as one removal plus one addition. + +Matching instead on `(ruleId, path, startLine, startColumn, endColumn)` and ignoring the +flow count: + +| | count | +|---|---:| +| findings genuinely lost | **25** | +| findings genuinely new | 1 (conductor path-format change — not a real gain) | +| findings present in both | 1213 | +| of those, flow count changed | 28 (15 down, 13 **up**) | + +WebGoat and maku-boot appear as failures but lost nothing at all. + +## 2. Where the 25 died — root causes + +Every loss now has an identified source. Counted per rule, not per project: per-project totals +mislead, because shopizer's two lost sql-injection discoveries were masked by two spurious ssrf +discoveries appearing. + +| findings | exact source | commit | ap-mode dependent | +|---:|---|---|---| +| **11** | `isTransparentToFact` — forward-only optimisation with no backward mirror | `2e5cf6f40` | BaseOnly-gated | +| **8** | ClassStatic pruning — two soundness inversions | `c7c1b0603` | BaseOnly-gated | +| **2** | override-cache re-key vs Spring Data custom fragments | `86481be76` | **no** | +| **1** | both of the above, compounded (Stirling `LicenseKeyChecker.java:98`) | `c7c1b0603` | BaseOnly-gated | +| 2 | taint on a boxed primitive — **accepted behaviour, dismissed** | — | — | +| 1 | not a loss — class name became a file path (conductor) | — | — | + +None of this is resource truncation. Across all eight affected projects there is no timeout, no phase +failure, and no memory-guard event; every shallow scan converged in 0.45–19.6 s against a ~420 s +budget. None of the losses is a budget effect — every `no_trace` outcome converged in 1–351 ms +against a 10 s per-vulnerability limit. + +Prescan rule selection is exonerated: the `Select N from 6048 rules` line is identical between base +and new on every project. + +**The gates chain.** Within a project these causes are independent but sequential, so fixing one +alone frequently recovers nothing — the finding simply dies at the next gate. Partial fixes will +look like failures. This was measured directly: ablating the ClassStatic prune restored discovery on +one sample but the finding still died at the rule-search `Failed` drop. + +## 3. The 11 — a forward pass that never tells the backward pass what it skipped + +`JIRAnalysisManager.kt:415-444`, gated at :422 with `if (apManager !is BaseOnlyApManager) return +false`. When a statement cannot touch a fact, `MethodAnalyzer.kt:391` calls +`propagateTransparentFactGroup`, which jumps the fact to the transparent closure's boundary and +**writes no `MethodAnalyzerEdges` entry for the skipped statements**. + +The backward trace resolver walks the CFG statement by statement and probes +`MethodTraceResolver.kt:2313 containsEntryEdge`, which finds the storage empty. It returns false not +because any access-path relation disagreed, but because the candidate set is empty before any +comparison happens. From there: `applicableEdges.isEmpty() -> continue`, `actions.isEmpty() -> +return`, `startNodes.isEmpty()`, requests exhausted, `TraceResolver.kt:252 return NoTrace`. + +The broadest clause is line 436 — any statement that is not an assign, return or throw (an `if` +branch, a goto) is transparent for **every** fact. + +Measured end to end on tms with the local project model, varying only the shallow-scan knob: + +| variant | findings | `FileController:433` | `no_trace` | `Failed` | +|---|---:|---|---:|---:| +| BaseOnlyField (shipped) | 152 | missing | 24 | 27 | +| + ClassStatic prune disabled | 152 | missing | 24 | — | +| + alt-premise cube fallback | 152 | missing | 0 | 27 | +| **+ transparent statements disabled** | **153** | **present** | **0** | **0** | +| Tree shallow scan | 153 | present | 0 | 0 | +| base `6adc217f2` | 153 | present | — | — | + +Necessary and sufficient. **A bug, not a consequence of field-insensitivity** — no access-path +relation is consulted anywhere in the chain, and `enableTraceResolutionMode()` cannot help because it +cannot re-create omitted edges. + +Also found and disproved as the cause: the alternative-premises guard +(`MethodTraceResolver.kt:764-766`). BaseOnly conjoins alternatives into one `SummaryTrace` instead of +Tree's cartesian product, so the guaranteed-non-empty shortcut is skipped. Adding a cube fallback +took `no_trace` 24 -> 0 but the finding stayed lost. It is a genuine defect — +commit `4e8082f4d`'s claim that nothing is lost there is false, since the cube fallback only fires on +`actionHardLimitReached` — but it is not the root cause. + +## 4. The 8 — ClassStatic pruning prunes in the wrong direction + +Both defects come from `c7c1b0603` and are gated `if (apManager !is BaseOnlyApManager) return true`, +so they never run in Tree. That is why base was unaffected, and why they present as BaseOnly problems +when they are not. + +**Defect 1 — an unresolved callee is treated as "cannot observe."** `JIRAnalysisManager.kt:464`: + +```kotlin +JIRCallResolver.MethodResolutionResult.MethodResolutionFailed -> Unit +``` + +The loop returns `false` (relevant) only when it finds a relevant target; an unresolved callee +contributes nothing and the function falls through to `return true` = "definitely irrelevant". +Unknown is read as safe when it must be read as relevant. This is fatal because OpenTaint compiles +every Spring `@Autowired` read into a non-deterministic stub whose `nextBool()` never resolves — so +every ClassStatic fact is transparent at every bean-injection site. The same gate is also blind to +receiver aliasing: a callee reading `this.metadataDao` records zero static accessors, so the call is +pruned even though the caller loaded that receiver from the registry. + +**Defect 2 — the most general fact is judged to observe nothing.** +`JIRClassStaticFootprintIndex.factMayObserve` (lines 394-404) reads the footprint path off the fact; for `EMPTY_ACCESS` +(`/{}`) — the whole-static-heap fact that crosses every intermediate frame — `readAccessor` +returns null on the first accessor. It implements "fact contains access" where the correct predicate +is overlap. That is why the failure needs exactly one extra frame: writing then calling `run()` +directly survives, while `runIt(task) -> task.run()` and `Thread.start() -> t.run()` both die. + +Measured ablation, showing the two are independently sufficient and that which one binds depends on +call shape: + +| ablation | cross-entry-point sample | group-A samples | +|---|---|---| +| tip | discovery **0** | discovery 2 -> `Failed` | +| `mayObserve` off | discovery 1 -> still `Failed` | discovery 2 -> `Failed` | +| unresolved-callee fixed | discovery 0 | **`Collected` -> found** | +| both | **found** | **found** | + +`overApproximateMethodContext` is refuted as a cause — measured, no effect. + +A separate defect worth its own ticket: the footprint index is a one-shot snapshot of `contexts()` +taken under `getOrBuildIndex()`, so pruning depends on method analysis order and is +non-deterministic. + +## 5. The 2 — a correct fix that removed a load-bearing accident + +`86481be76` re-keyed `JIRCallResolver.methodOverridesCache` from `JIRMethod` to +`Pair`. The new key is correct; the problem is that the old, wrong +key was load-bearing. + +Shopizer uses the Spring Data custom-fragment pattern: + +```java +interface ProductRepository extends JpaRepository, ProductRepositoryCustom { } +class ProductRepositoryImpl implements ProductRepositoryCustom { } // NOT a ProductRepository +``` + +The call receiver at `ProductServiceImpl.java:353` is typed `ProductRepository`, so +`findOverrides(method, baseClass = ProductRepository)` correctly cannot return +`ProductRepositoryImpl`. Base reached it only by cache aliasing across call sites. +`ProductRepositoryCustom` and `ProductRepositoryImpl` are referenced by type nowhere else in +shopizer, so nothing else registers the impl. + +This loss is **ap-mode independent** — both `Tree` and `BaseOnlyField` give `raw=0`, and restoring +the old cache key restores the finding in both. Corroborating: shopizer's prescan, which is +TreeApManager in both runs, drops 54,068 -> 45,437 steps, and `ProductRepositoryImpl#listByStore` +goes from `steps 1118 | sum 200` to `steps 521 | sum 0` — it never produces a summary in any phase. +The bridge-argument filter and the unconditional `alwaysIgnoreMethod` were ablated individually and +exonerated. + +Do not fix by reverting the cache key — the old behaviour was accidental and order-dependent. Model +the Spring Data fragment relation explicitly instead. + +## 6. Stirling `LicenseKeyChecker.java:98` — a true positive found for the wrong reason + +The vulnerability is real, and verified in the scanned sources: `saveLicenseKey(@RequestBody +Map)` -> `request.get("licenseKey")` -> `updateLicenseKey` -> +`applicationProperties.getPremium().setKey(newKey)` -> `getLicenseKeyContent` -> `substring` -> +`Paths.get` -> `Files.exists` / `Files.readString`, with no sanitization on that path. + +But **neither analyzer ever derived that store.** Across all 891 threadFlow steps of the base run, +not one assign/propagate-to target names `ApplicationProperties`, `Premium` or `key`. Base reported +the finding via an over-approximated ClassStatic start plus a spurious parameter-hop prefix — the +right answer for the wrong reason. Restoring it through the rule-selection fix alone would bring it +back with the same bogus trace; deriving it properly needs the `@RequestBody Map` element-propagation +gap fixed, which is a separate issue. + +## 7. thingsboard: the OOM + +The status is `[complete, high_memory, oom]`, 473 s, exit 253. No `OutOfMemoryError` +appears in any log and the SARIF is complete with 14 findings identical to base. + +Exit 253 comes from the soft watchdog: `OOM_DETECTION_THRESHOLD = 0.90` +(`TaintAnalysisUnitRunnerManager.kt:839`) of the 12 GiB cap. Its handler calls +`cancellation.cancel()`, so **the shallow scan was aborted at 13:44:34, not converged** — +14,594 events were still enqueued. Already-found discoveries survive +(`vulnerabilityBuckets` is untouched by `cancel()`), but since the full scan only arms +rules at discovered statements, any corridor the aborted scan never reached is silently +unarmed. It happened to still find all 14; that is luck, not a property. + +`updateFailureStatus` is `compareAndSet(OK, status)`, so a preparatory phase's OOM is +sticky and turns a complete, correct run into a reported failure. + +### Where the time went + +Base is **not** single-phase — it runs prescan + full scan. + +| stage | base | new | Δ | +|---|--:|--:|--:| +| load + IR | 69 s | 71 s | +2 | +| prescan | 180 s | 139 s | **−41** | +| shallow scan | — | 210 s | **+210** | +| full scan | 65 s | 40 s | **−25** | +| SARIF | 5 s | 9 s | +4 | +| **total** | **319 s** | **469 s** | **+150** | + +The perf commits paid off — prescan −23%, full scan −38%. The entire regression is the new +phase. + +### Why the "cheaper" mode costs more: the throttle is dead code + +`MethodAnalyzer.kt:774-778` is the engine's only fact-explosion backpressure: + +```kotlin +if (edge.initialFactAp.depth > factDepthLimit) return true // factDepthLimit starts at 3 +if (edge.factAp.depth > factDepthLimit + 2) return true // i.e. > 5 +``` + +`INITIAL_ALLOWED_FACT_DEPTH = 3` (`MethodAnalyzer.kt:1855`). BaseOnly's `depth` is +`BaseOnlyAccess.size`, which counts three optional slots and is therefore 0–3 +(`BaseOnlyAccess.kt:148-155`, `BaseOnlyFinalFactAp.kt:27`, `BaseOnlyInitialFactAp.kt:23`). +**Both clauses are unreachable in BaseOnly mode.** `registerDelayed` never fires and no +unit ever escalates its fact limit during the shallow scan. + +Measured: 1,293 `Increase unit fact limit` events in the new run — 46 in prescan, 1,198 in +the 40 s full scan, and **zero in 188 s of shallow scan** (its 49 are all stamped at the +second prescan ended). Field-insensitivity shrinks each fact but multiplies distinct facts, +and nothing throttles that: the shallow scan performs 1,738,208 fact-type checks, 2.1× base's +entire full scan. `AccessValidator.validateApiUsageState` went from 514 handled summaries in +base's whole run to 111,516. + +Live-set decomposition at the trip (post-GC values only — the `Memory usage:` log lines are +`maxMemory − freeMemory` and include uncollected garbage): + +``` + ~5.4 GB baseline live, also present in base ++~2.3 GB added by shallow, NOT released at the phase switch ++~3.8 GB shallow-phase-local, released at the switch +=~11.6 GB vs an 11.60 GB threshold +``` + +Live then reached 12.87 GB (99.9%) during actionable-rule trace resolution — about 1.3 GB +added by resolving 23 traces in 20 s. + +## 8. codeFlows counts + +Only 28 of 1213 surviving findings changed flow count, and 13 of those went **up**. The +governor is not new: `MethodTraceSearch.selectMethodTraces` (`MethodTraceSearch.kt:184-282`) +is a greedy set-cover over *method nodes*, not a path enumerator — line 244 stops as soon as +every method node is covered, even for `(sink, source)` pairs never connected. That file is +byte-identical between base and new; the counts moved only because `4e8082f4d` quotiented +the graph. No hard cap fired. + +Where counts fell, the dedup is correct: WebGoat `UserService:53` 6→2 and Stirling +`AdminLicenseController` 8→1 drop only flows that are the identical genuine tail prefixed by +a bogus `Exiting X / Entering Y` hop between handlers that never call each other. Both +genuine paths survive. + +Where counts rose, quality regressed: hertzbeat 1→2 *adds* such a spurious prefix, another +hertzbeat finding swaps its representative for a bogus-prefixed one, and tms +`BlogController:1637` loses the genuine flow entirely. + +The representative is not deterministic — `generalizedStart2FinalTraceCache` +(`TraceResolver.kt:606-637`) is a `ConcurrentHashMap` with first-writer-wins under parallel +workers. + +## 9. What to fix + +Recall. These must land **together** — the gates chain, so any one alone recovers little and will +look like a failed fix. + +1. **Done — `isTransparentToFact` and its transparent-closure machinery removed.** The backward mirror was the alternative: teach the + trace resolver to treat a collapsed statement as edge-present and continue to its predecessors. + It was not taken. Both benchmarks were performance-equivalent without the optimisation, so the + forward-only pruning was deleted instead of being made two-sided, and the branch no longer + contains it. Recovers 11. +2. **Done — `JIRClassStaticFootprintIndex` and its call-skipping consumers removed.** Repairing it needed two changes that pull against + each other: read an unresolved callee as relevant rather than irrelevant, and make the + observation test an overlap rather than a containment test — the second re-admits exactly the + whole-static-heap fact the optimisation existed to prune, so the repaired version buys close to + nothing. The footprint index, the forward skip and its backward mirror were deleted. Recovers 8, + plus the compounded Stirling finding. +3. **Kept as tests.** The samples and regression tests that came with the skipped-call work are + retained; they now pin the recall the removal restores. +4. **Route `Failed` into the forward fallback** alongside `Unprocessed` + (`HybridActionableRuleSelection.kt:12-17`). JVM already sets + `supportsForwardActionableRuleFallback = true`. Required for 2 and 3 to show any benefit at all. +5. **Done — Spring Data fragment relation modelled.** A constrained override lookup that finds + nothing now retries once against the method's own declaring interface, when that interface is + itself in the project. The re-keyed override cache was *not* reverted; the old behaviour was + accidental and order-dependent. Recovers 2. +6. **Do not let an empty shallow result zero the scan** (`TaintAnalyzer.kt:143`) — fall back to an + unrestricted full scan. Converts a silent total-recall loss into a bounded slowdown. +7. **Stop narrowing rule actions** (`SelectedTaintRulesProvider.kt:232`). Free recall, low risk. + +Because 2 and 3 give back most of the shallow-scan speed the optimisations bought, on a project +already at the OOM watchdog, they must be benchmarked together with the memory work below. + +Memory, in landing order: + +4. **F5 — reassign instead of `clear()`** in `resetEdgeProcessingStorage` + (`MethodAnalyzer.kt:1831-1836`), `TaintAnalysisUnitRunner.kt:99`, + `SummaryEdgeSubscription.kt:861`; reset `methodEntryPointsCache` and + `registeredResolvedCallees` in `resetApManager`, not only `cleanup()`. Mechanical, + behaviour-identical. +5. **F6 — gate `MethodTaintMarkReachabilityIndex.addSummaryEdges` on `Phase.ShallowScan`** + and clear `callers`/`callees` in `resetApManager`. Zero risk; the index is read only on + the shallow fallback path. +6. **F3 — free the trace caches per vulnerability, not per phase** + (`MethodAnalyzer.kt:228`, `JIRMethodAnalysisContext.kt:75`). Measured ~1.3 GB. +7. **F2 — cap the remaining per-analyzer BaseOnly memos.** The largest of them, the + statement-collapse closure map, is gone with the optimisation; the rest are still uncapped. + Cap, do not delete — removing them wholesale was measured at 124 s → 159 s. +8. **F1 — give the shallow scan a throttle that can fire**: a count-based criterion in + `edgeExceedLimit`, or per-ap-mode depth semantics. This is the only change that *bounds* + the phase. Without it the OOM recurs on any larger codebase. +9. **F7 — stop a preparatory phase's OOM from poisoning the exit code.** Land *after* F1, + since alone it hides the recall risk rather than fixing it. + +Trace quality: + +10. In `MethodTraceSearch.kt:244`, track covered `(sink, source)` pairs separately from + covered method nodes and accept a Pass-1 trace whenever its pair is uncovered, so every + reachable source/sink pair gets a flow. +11. Penalise starts with `isStartOverApproximation = true` + (`MethodTraceResolver.kt:778,830`) so a direct entry-point start outranks a + boundary-hopped one. + +## 10. Caveats on this data + +- One run per side. This suite is known to be non-deterministic, and the trace representative + is explicitly first-writer-wins under concurrency. Re-run before treating any single count + as stable. +- The base results are dated 2026-08-13 and were reused from cache + (`probe-thingsboard-base` shows `hit: true`); the new results ran fresh on 08-19. + Finding-set comparisons hold; cross-day wall-clock comparisons are weak. +- When diffing SARIFs, match on `(ruleId, path, startLine, startColumn, endColumn)` and + ignore the `codeFlows` count, or the report will overstate the damage as it did here. diff --git a/docs/forward-derived-actionable-rules-design-2026-07-28.md b/docs/forward-derived-actionable-rules-design-2026-07-28.md new file mode 100644 index 000000000..bd03e56a1 --- /dev/null +++ b/docs/forward-derived-actionable-rules-design-2026-07-28.md @@ -0,0 +1,244 @@ +# Forward-derived actionable-rule selection + +## Goal + +Replace shallow-scan trace resolution as the mechanism that selects rules for +the full scan. The forward analysis already evaluated every source rule/action +that can contribute a fact. Recording those successful applications is much +cheaper than reconstructing all action-bearing traces. + +The effective full-scan selection contract is: + +```text +Map>> +``` + +Sinks use an empty action set. Today `SelectedTaintRulesProvider` filters source +rules and sinks with this map. Pass-through and cleaner rules are delegated and +therefore do not need selection provenance yet. + +## Experimental implementation + +The experiment is disabled unless the JVM property +`opentaint.experimental.forward-actionable-rules=true` is set. It does not +change production rule selection. + +`ForwardActionableRulesRecorder` is owned by each +`TaintAnalysisUnitStorage`. Its state is cleared together with facts, +summaries, and vulnerabilities by `resetApManager`. All unit snapshots are +merged by `TaintAnalysisUnitRunnerManager`. + +The JVM forward-analysis hooks record a source `(statement, rule, action)` only +after the evaluator produced an output fact: + +- `JIRMethodCallTaintUtil#applySourceAction`: method-call sources, after + exit-to-return mapping succeeds. +- `JIRSequentTaintUtil#applySourceAction`: method-exit sources. +- `JIRMethodStartFlowFunction#propagateZero`: entry-point sources. +- `JIRMethodSequentFlowFunction#applyUnconditionalSources`: static-field + sources. + +Trace-recomputation calls are excluded. Confirmed shallow vulnerabilities add +their sink rules with an empty action set. + +After normal trace-based actionable-rule search, `TaintAnalyzer` compares exact +atoms: + +```text +(statement, rule, action?) +``` + +`action=null` denotes a sink. It logs counts plus every forward-only and +trace-only atom. A trace-only atom is a safety blocker: it proves that the +forward recorder missed something required by the current implementation. +The report emits both raw trace contents and the effective JVM-provider +subset. Raw pass-through and cleaner actions are excluded from the effective +comparison because `SelectedTaintRulesProvider` delegates those categories. + +## Semantics + +### Cheap global selection + +The experimental map is deliberately a global over-approximation: + +```text +all source actions that emitted a shallow-forward fact + union +all sink rules of confirmed shallow vulnerabilities +``` + +It may include source actions whose facts never reach a confirmed sink, are +later cleaned, or are used only in another calling context. This can increase +the full-scan workload, but it cannot create a vulnerability by itself: the +full forward analysis must still establish source-to-sink reachability. + +If every source-producing operation is instrumented, the expected relation is: + +```text +effective, forward-representable trace-selected atoms + ⊆ forward-derived atoms +``` + +The qualification matters. Trace recomputation currently admits pass-through +actions that the selected provider delegates anyway, and it can reconstruct +facts on primitive values that forward analysis intentionally refuses to +store. Neither category is an actionable forward-source requirement. + +### What a flat global set cannot preserve + +The current trace search preserves source/sink correlation and rejects a +vulnerability when no valid nested summary trace can be resolved. A global +forward set does neither. Therefore it is suitable as: + +1. a safe full-scan rule over-selection mechanism, and +2. a way to remove actionable-rule trace resolution from the critical path, + +but not as a replacement for final vulnerability trace validation. + +Summary subsumption and field generalization make a flat provenance set even +less precise. If provenance is attached directly to a generalized summary +edge, provenance from a narrower removed edge would be incorrectly available +to every application of the generalized edge. + +Persisted summaries also create a completeness requirement. On a cache hit, +`MethodAnalyzer#loadSummariesFromRunner` installs serialized edges without +executing the source evaluators that the experimental hooks observe. A source +action represented only by such a summary can therefore be missing from the +forward-derived set. + +Before production use, persisted summaries must carry conservative +method-level source provenance: + +```text +Map>> +``` + +The provenance is serialized beside the summary, unioned when summaries are +loaded or applied, and versioned with the summary format. An old summary +without provenance must be invalidated/recomputed (or conservatively fall back +to trace-based rule selection). Method-level union is sufficient for the +global over-selection design. It is not sufficient for the exact +per-vulnerability design below. + +## Exact per-vulnerability design, if global selection is too broad + +Record a compact proof dependency DAG during the shallow forward analysis. +Each canonical forward edge points to proof nodes: + +```text +LocalAction(statement, rule, action, predecessor) +Flow(predecessor) +SummaryApply(callerPredecessor, summaryProof) +Join(predecessors) +``` + +At a sink, store the proof-root identities together with the vulnerability +fact group. After confirmation, traverse only those roots and union their +`LocalAction` tokens. + +Required invariants: + +- When a known edge gains a new proof predecessor, enqueue the proof update + even though the fact itself is not new. +- Summary storage keeps guarded proof alternatives. Subsumption may redirect a + removed summary edge to a surviving edge, but must not flatten the removed + edge's provenance into an unconditional token set. +- An N-dimensional edge records dependencies on all participating initial + facts. +- Conditional rule tokens are added only after the condition succeeds and the + action emits a fact. +- Sink proof roots retain the exact trigger position and fact group. + +This design preserves correlation without materializing `FullTrace`, but it is +substantially more invasive than global selection and can have edge-by-proof +growth. It should be implemented only if measurements show that the global +over-selection makes the full scan too expensive. + +## Conductor experiment + +The gated experiment was run with `--ifds-ap-mode BaseOnlyField` on the +Conductor project and its project-specific rules/approximations. + +Raw trace contents: + +```text +forward=6616, trace=2270, common=1039, +forward-only=5577, trace-only=1231 +``` + +Of the 1231 raw trace-only atoms, 1229 are pass-through actions. They are not a +safety blocker because the selected provider always delegates pass-through +rules. + +The effective provider-selected comparison is: + +```text +forward=6616, trace=1041, common=1039, +forward-only=5577, trace-only=2 +``` + +The two trace-only source atoms are: + +1. `java.lang.String#getBytes()` assigning a mark to `Result.Element` (`byte`). +2. `WorkflowModel#getPriority()` assigning a mark to `Result` (`int`). + +Both are primitive/primitive-element results that the forward analysis +intentionally drops. They can appear in trace recomputation, but cannot +contribute a stored forward fact under the strict primitive policy. Therefore +there are zero non-primitive effective trace-only atoms. + +Timing from this run: + +```text +prescan 28.08s +shallow forward 27.01s +actionable rule search 43.60s +``` + +The global forward map is available immediately after shallow scan; replacing +rule search would remove the observed 43.60-second phase. Its 5577 additional +atoms mean the full-scan cost must be measured before rollout. The current +trace-selected full scan in this run took 17.58s; shallow time (27.01s) is a +conservative first-order upper-bound signal, not a substitute for a direct +forward-selected full-scan measurement. + +The experiment is observational: it still runs trace-based actionable-rule +search and still feeds the trace-selected map to the full scan. It proves the +set difference, but it does not yet prove the end-to-end time or memory of a +forward-selected full scan. + +## Bypass modes + +There are two distinct deployment choices: + +1. **Bypass actionable-rule trace search only.** Keep shallow vulnerability + confirmation, union successful forward source actions with the confirmed + sink rules, and feed that map to the full scan. This removes the expensive + `TraceActionSearcher` phase while retaining the existing shallow + confirmation gate. +2. **Bypass all shallow backward work.** Union successful source actions with + sink rules from raw shallow vulnerabilities. This is more conservative and + avoids shallow confirmation, but it can select additional sinks and further + increase full-scan work. Final vulnerability confirmation and trace + generation remain mandatory correctness gates. + +Mode 1 is the initial rollout target. Mode 2 should be evaluated only after +Mode 1 has matching final findings and acceptable full-scan cost. + +## Mitigation rollout + +1. Add persisted-summary provenance and tests for generate/store/load/apply. +2. Run the gated comparison on Conductor and representative unit/querylang + suites. Require zero non-primitive effective trace-only source atoms. + Sink-only differences caused by trace-search failures must be reported + separately. +3. Add an analyzer option for bypass Mode 1; keep final full-scan + confirmation and trace validation. +4. Compare final finding identities, full-scan time, peak memory, and status + against trace selection. The intended improvement is elimination of the + actionable-rule trace-resolution phase. +5. Add the equivalent successful-source hooks and summary provenance for Go + before enabling the mechanism in the common staged analyzer for Go. +6. Evaluate bypass Mode 2 separately. +7. If global over-selection is too large, implement the proof-DAG refinement + rather than reintroducing eager `FullTrace` materialization. diff --git a/docs/thingsboard-shallow-fact-explosion-2026-08-06.md b/docs/thingsboard-shallow-fact-explosion-2026-08-06.md new file mode 100644 index 000000000..f52d7f318 --- /dev/null +++ b/docs/thingsboard-shallow-fact-explosion-2026-08-06.md @@ -0,0 +1,229 @@ +# ThingsBoard shallow fact explosion + +Date: 2026-08-06 + +## Verdict + +The current ThingsBoard shallow-scan cost is not caused primarily by field enumeration. It is a +product of three independent dimensions: + +```text +method type contexts × initial-to-final fact alternatives × branch-heavy CFG statements +``` + +The dominant repeated facts are already suffix-abstract (`.*`). Consequently, lowering the F2F +summary field-generalization threshold cannot collapse them. Summary generalization also runs only +after a path edge reaches a method exit, after the intraprocedural cost has already been paid. + +The first mitigation should be a transitive class-static access footprint. It allows global facts +to bypass callees that cannot observe or modify them, without merging contextual fact sets or +weakening virtual-call resolution. + +## Concrete source pattern + +Two generic service methods account for the largest repeated work: + +- `EntityActionService#pushEntityActionToRuleEngine` accepts the interface/base values + `EntityId`, `HasName`, and `User`, contains a long `if/else` chain, and calls methods on all three + values. +- `AuditLogServiceImpl#constructActionData` is reached through generic + `` callers and contains a large `switch (actionType)`. + +Representative source: + +```java +public void pushEntityActionToRuleEngine(EntityId entityId, HasName entity, ..., User user, ...) { + ... + metaData.putValue("userName", user.getName()); + ... + entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(entity); + metaData.putValue("entityName", entity.getName()); + metaData.putValue("entityType", entityId.getEntityType().toString()); + ... +} +``` + +```java +private JsonNode constructActionData( + I entityId, E entity, ActionType actionType, Object... additionalInfo) { + ObjectNode actionData = JacksonUtil.newObjectNode(); + switch (actionType) { + case ADDED: + case UPDATED: + ... + case ATTRIBUTES_UPDATED: + ... + // many more cases merge at one exit + } + return actionData; +} +``` + +`JIRCallResolver.MethodContextCreator#createContexts` materializes the Cartesian product of the +receiver/argument type alternatives. Across the observed run this produced: + +| Method | distinct contexts | +|---|---:| +| `pushEntityActionToRuleEngine` | 108 | +| `constructActionData` | 62 | + +The contexts include concrete pairs such as `(AssetId, Asset)`, `(DeviceId, Device)`, +`(RuleChainId, RuleChain)`, and variants with `SecurityUser`. + +## Fact and statement evidence + +A diagnostic classified every processed edge in the two methods. + +### `pushEntityActionToRuleEngine` + +```text +recorded steps: 224,330 +F2F: 187,286 +Z2Z: 37,044 +all non-zero shapes: abstract suffix +ClassStatic bases: 126,038 +Argument bases: 21,301 +Local bases: 20,406 +This bases: 19,541 +contexts: 108 +``` + +The hottest statements are joins and parameter-to-local assignments: + +```text +3,745 goto index 294 +3,566 goto index 334 +3,498 %186 = entityNode +3,444 %183 = additionalInfo +3,438 %181 = actionType +3,437 %182 = user +``` + +### `constructActionData` + +```text +recorded steps: 118,065 +F2F: 102,069 +Z2Z: 15,996 +all non-zero shapes: abstract suffix +ClassStatic bases: 59,516 +Argument bases: 18,986 +Local bases: 14,502 +This bases: 9,065 +contexts: 62 +``` + +The common switch join alone was processed 8,846 times: + +```text +8,846 goto index 241 +1,605 return actionData +1,435 goto index 64 +``` + +The class-static facts include generated Semgrep automaton state, for example: + +```text +(java/security/xss.yaml:xss-in-spring-app;sink_135;__;pos).*/... +(java/security/xss.yaml:xss-in-spring-app;sink_136;__;pos).*/... +``` + +## Exact operation chain + +1. `TaintRuleGenerationCtx#stateVarPosition` represents a global automaton state as a + `PositionBase.ClassStatic` value. +2. `BaseOnlyInitialFactAbstraction#abstractOneBranch` turns it into a compact suffix-abstract + BaseOnly fact. +3. `JIRMethodCallFactMapper#factIsRelevantToMethodCall` returns `true` for every + `AccessPathBase.ClassStatic`, without considering the callee. +4. `JIRMethodCallFactMapper#mapMethodCallToStartFlowFact` copies the fact unchanged into every + resolved callee. +5. `JIRCallResolver.MethodContextCreator#createContexts` creates context-specific callees. +6. `MethodAnalyzerStorage#add` creates a separate analyzer for every full `MethodEntryPoint`. +7. `MethodEdgesInitialToFinalBaseOnlyApSet` preserves the initial-to-final correlation at every + statement, so each context traverses the large switch/branch body for every surviving + alternative. + +There is no single incorrect BaseOnly access-path operation in this chain. The representation is +compact per fact, but the engine schedules the same global-state problem once per local type +context. + +## Why field generalization does not address it + +- Every sampled non-zero fact in the hot methods was already suffix-abstract. +- `BaseOnlyF2FFieldGeneralizer#eraseFieldForSummaryGeneralization` rejects accesses with a static + slot. +- `MethodInitialToFinalBaseOnlyApSummariesStorage` sees an edge only at method exit. It cannot + remove work inside the method being summarized. +- Raising/lowering the summary threshold can reduce downstream summary dispatch, but cannot + remove the `contexts × facts × statements` product in these methods. + +## Rejected experiments + +| Experiment | Result | Reason | +|---|---|---| +| Route all shallow facts through `EmptyMethodContext` | Did not finish in the normal window | Losing receiver constraints greatly widens virtual dispatch | +| Route only `ClassStatic` facts through `EmptyMethodContext` | About 3.20M steps, effectively unchanged | Hot methods shrink, but unconstrained dispatch moves the work into callees | +| Join exact contexts into disjunctive type sets only for `ClassStatic` | 3.66M steps; shallow 100.4s | Alternatives cross-pollinate and create more summaries/facts | +| Persist exact unchanged-edge deduplication | No step reduction; substantially more retained memory | Exact duplicate replay is not the dominant term; alternatives differ by facts/exclusions | +| Store unchanged BaseOnly edges in the normal fact set | 3.64M steps; shallow 111.2s | Fact-state merging/republication costs exceed duplicate savings | + +The experiments were diagnostics only and were reverted. + +## Mitigation design + +### 1. Build a transitive class-static footprint + +For each analyzable method, collect the class-static accessors that the method may observe or +modify: + +- explicit static field reads/writes; +- taint-rule conditions and actions using a `ClassStatic` position at method entry/exit or a call + statement; +- the footprints of every possible callee, including all conservative virtual/lambda targets. + +Compute the union to a fixed point over the conservative call graph. Recursive SCCs share one +fixed-point value. + +### 2. Filter after concrete call resolution + +The current `factIsRelevantToMethodCall` check happens before a concrete callee is known. Keep the +ordinary local/argument relevance test there, but check a `ClassStatic` fact against the resolved +callee footprint in `JIRMethodCallResolver` before creating/subscribing to its analyzer. + +- A concrete static accessor is propagated only if it belongs to the footprint. +- A wildcard static fact is propagated only if the footprint contains an accessor not removed by + its exclusions. +- If the fact is irrelevant, apply the identity call-to-return effect; do not drop it. + +This preserves the caller fact while avoiding the callee CFG and its method contexts. + +### 3. Preserve eventual consistency + +The footprint may grow when a lambda or a newly resolved virtual target appears. A growth event +must revisit existing class-static call subscriptions. Publication is monotone: accessors are only +added, never removed. + +### 4. Keep context precision + +Do not replace the callee context with `EmptyMethodContext`, and do not union independent type +alternatives in one fact set. The footprint filter removes irrelevant global work before analyzer +creation while leaving ordinary type filtering and dispatch unchanged. + +### 5. Test obligations + +1. A class-static state changed directly in a callee must be propagated. +2. A state changed only in a transitive callee must be propagated. +3. An irrelevant callee must return the state unchanged without creating its analyzer for that + state. +4. A late lambda/virtual target must grow the footprint and activate an existing subscription. +5. Tree/BaseOnly differential dataflow tests must show no lost reachability. +6. ThingsBoard must retain the same shallow discoveries and final sink hashes while reducing the + two hot methods' `ClassStatic` steps. + +## Secondary direction + +Generated branch-heavy methods with no summary callbacks (for example protobuf +`buildPartial0`) are a separate intraprocedural problem. A summary-storage generalizer cannot +reduce their own CFG work. Address them later with generated-code summaries or a separately +specified fact-set widening policy; neither should be mixed into the class-static footprint fix. diff --git a/docs/trace-action-searcher-design.md b/docs/trace-action-searcher-design.md new file mode 100644 index 000000000..d4452ee15 --- /dev/null +++ b/docs/trace-action-searcher-design.md @@ -0,0 +1,868 @@ +# Trace action searcher design + +Date: 2026-07-23 + +## Status + +Implemented by `TaintAnalysisUnitRunnerManager.collectActionableRules` and +the staged JVM/Go rule providers. This document remains the behavioral +contract for the implementation. + +## Goal + +The shallow scan must identify the configuration entries that are sufficient +to reproduce each resolved vulnerability in the full scan. Across every +trace branch that can participate in a complete source-to-sink path, the +searcher must collect: + +1. the sink rule represented by the vulnerability, with an empty action set; +2. every source rule and its actions carried by an `otherAction` in the + relevant trace; pass-through rules are not collected; +3. source rules and actions hoisted into a `SourceStartEntry`; +4. source rules and actions inside every `CallSummary` that introduces or + changes a taint mark. + +Change `Collected.rules` to expose a +`Map>`. An empty action +set denotes a sink rule. A non-empty action set contains every used action for +that rule. + +The searcher does not prove the vulnerability again. `TraceResolver` has +already built the interprocedural source-to-sink graph. The searcher identifies +the graph corridor that belongs to at least one complete source-to-sink path, +materializes its `FullStart2FinalTrace` objects, expands relevant inner +summaries, and projects all relevant entries to the rule/action map. + +## Non-goals + +- Do not enumerate source-to-sink or intra-method path combinations. Traverse + every entry in every relevant full trace and recursively traverse every + relevant summary. The required result is a union, so path enumeration adds + combinatorial cost without adding information. +- Do not collect rules from entry-point-to-start traces. The selected rules + describe taint creation and propagation from source to sink, not ordinary + reachability from an application entry point. +- Do not expand a method summary whose before/after taint-mark sets are equal. +- Do not expand structural summaries whose boundary facts are all abstract + and unmarked. +- Do not infer markedness from AP implementation classes, `isAbstract()`, or + from `SourceTraceEdge` versus `MethodTraceEdge`. +- Do not make path order part of the result contract. + +## Existing model + +### Trace representations + +`MethodTraceResolver` has three relevant representations: + +| representation | contents | use | +|---|---|---| +| `SummaryTrace` | method, final entry, trace kind | lazy request for an intra-method trace | +| `Start2FinalTrace` | method, selected start, final, trace kind | compact interprocedural graph node | +| `FullStart2FinalTrace` | entry array, start/final IDs, successor graph | materialized intra-method witness | + +`TraceResolver.Trace.sourceToSinkTrace` connects compact +`Start2FinalTrace` nodes. `trace/path/Source2SinkTraceGraph.kt` separates the +root-to-source and root-to-sink directions. +`trace/path/TracePath.kt` shows how compact nodes are converted to +`FullStart2FinalTrace` objects. + +The action searcher should reuse those graph-building and full-resolution +operations. It should not use the reporting path sampler as its semantic +oracle: the sampler intentionally selects representative paths and one +intra-method route, while full-scan rule selection must not omit a relevant +alternative. + +### Rule-bearing entries + +`TraceEntry.Action` contains a primary action, a set of other actions, and +unchanged edges. The rule-bearing other-action variants are: + +| action | rule type | action type | +|---|---|---| +| `SequentialSourceRule` | `CommonTaintConfigurationSource` | `Set` | +| `CallSourceRule` | `CommonTaintConfigurationSource` | `Set` | +| `EntryPointSourceRule` | `CommonTaintConfigurationSource` | `Set` | +| `CallRule` | pass-through rule | ignored; pass-through rules remain globally enabled | + +`MethodTraceResolver.tryCreateSourceStart` converts a source-only +`TraceEntry.Action` to `TraceEntry.SourceStartEntry`. Therefore collection +must inspect both: + +```text +TraceEntry.Action.otherActions +TraceEntry.SourceStartEntry.sourceOtherActions +``` + +Inspecting only `TraceEntry.Action` would silently lose source rules. + +The primary action variants do not directly contribute rule/action map data: + +- `Sequential` and `UnresolvedCallSkip` are structural; +- `CallSourceSummary` points to the source-producing callee trace; +- `CallSummary` points to an optionally relevant inner callee trace. + +### Vulnerability rule provenance + +`TaintVulnerability` contains a map of sink rules to vulnerability rule +nodes. Trace resolution walks the node values but does not retain which map +key produced the selected trace. Consequently +`TaintVulnerability.rule`, which returns the first map key, is not reliable +when multiple sink rule objects were merged under the same vulnerability ID. + +The safe current behavior is: + +```text +for every vulnerability.vulnerabilityRules key: + collect sink rule -> emptySet() +``` + +This is a small overapproximation. If exact sink-rule provenance becomes +important, `TraceResolutionRequest` and `TraceResolver.Trace` must carry the +originating sink rule. Selecting the first map key is not an acceptable +substitute. + +## Relevant-entry specification + +### Vulnerability sink + +Every sink rule attached to the vulnerability is relevant. Emit one map entry +per rule: + +```text +sinkRule -> emptySet() +``` + +An empty set is reserved for sink rules. A trace-derived source rule must have +a non-empty action set. + +### Other actions + +For every source-rule-bearing other action in the relevant trace, union its +action set into the map value for its rule. Ignore pass-through actions: + +```text +RuleAction(rule = R, actions = {A1, A2}) + -> R -> {A1, A2} +``` + +The rule is the map key and actions deduplicate within its set. Repeated uses +of the same rule accumulate their action sets: + +```text +R -> {A1} +R -> {A2} + becomes +R -> {A1, A2} +``` + +The representation assumes that a configuration item cannot be both a sink +rule and an action-owning source rule. Enforce this invariant while +building and consuming the map; otherwise `emptySet()` would be ambiguous. + +### `CallSourceSummary` + +`CallSourceSummary` carries no direct rule/action map contribution. + +When it appears as the primary action of a `SourceStartEntry` in a full trace +materialized from an outer compact node, `TraceResolver` has created the +corresponding `CallToSource` interprocedural edge. The relevant-node corridor +includes the callee as a separate method trace. Its source and propagation +actions are therefore collected in their normal entries. + +In other words: + +```text +CallSourceSummary in SourceStartEntry + -> no direct map contribution + -> for an outer compact model, callee already appears on root-to-source + graph corridor +``` + +If the callee cannot be resolved, that source branch is invalid. It must be +pruned before the outer corridor is recomputed; the searcher must not silently +treat the caller entry as a complete source. + +There is one distinct case. `MethodTraceResolver.tryCreateSourceStart` does +not hoist a source action when the same entry also has unchanged edges or +when any sibling other action is not a `SourceOtherAction`. A +`CallSourceSummary` can therefore remain the primary action of an ordinary +`TraceEntry.Action`. + +The user's intended invariant is that this action is already on the +source-to-sink path. That is true for the caller action entry, but the current +interprocedural graph does not add a `CallToSource` callee node for an +internal action; it recognizes only a `SourceStartEntry` primary summary. +This design chooses an explicit compatibility path: resolve the internal +action's `summaryTrace` as an inner full trace. This does not duplicate the +`SourceStartEntry` case because the two entry variants are mutually +exclusive. A future trace-model change may represent every such source call +interprocedurally and then remove this fallback. + +A `SourceStartEntry.CallSourceSummary` found while recursively materializing +an inner summary is different: that inner model has no node in the outer +interprocedural graph. Treat its source summary as a required inner dependency +and resolve it recursively. + +### `CallSummary` + +`CallSummary` also carries no direct map contribution. Pass-through rules stay +globally enabled, so an inner method trace is relevant only if it can +contribute a source rule needed to establish a different taint mark. + +For every caller-side `summaryEdge`, compare the taint marks on +`edge.fact` before the call with the marks on `edgeAfter.fact` after the call: + +```text +SourceSummary -> expand +MethodSummary with beforeMarks != afterMarks -> expand +MethodSummary with beforeMarks == afterMarks -> skip +``` + +If a `CallSummary` combines several edges, expand when any edge requires +expansion. A `SourceSummary` is always relevant even when its concrete fact +happens to carry the same mark, because it explicitly represents zero-to-fact +source creation. + +The callee-side `summaryTrace.final.edges` predicate remains a secondary +guard: an all-abstract, unmarked callee summary is skipped. Concrete-unmarked +callee summaries are expanded only when the caller-side mark transition above +requires it. + +For a `TraceEdge`, its complete boundary fact set is: + +```text +SourceTraceEdge -> { fact } +MethodTraceEdge -> { initialFact, fact } +MethodTraceNDEdge -> initialFacts union { fact } +``` + +A summary operates on taint marks exactly when at least one boundary fact of +its final entry satisfies: + +```kotlin +fact.getAllAccessors().any { it is TaintMarkAccessor } +``` + +Both input and output facts are required because a summary can create, carry, +or remove a mark. + +`FactAp.isAbstract()` is not the markedness predicate. A fact may be abstract +and still carry a mark in the general Tree or Automata domain. + +| caller transition and summary boundary | decision | +|---|---| +| contains `SourceSummary` | resolve inner full trace | +| any method edge changes the taint-mark set and callee is relevant | resolve inner full trace | +| every method edge preserves its taint-mark set | skip inner trace | +| callee boundary is entirely abstract and unmarked | skip inner trace | + +```text +EXPAND if any caller summary edge introduces or changes TaintMarkAccessor + and the callee summary boundary is relevant +SKIP otherwise +``` + +## Proposed pipeline + +The implementation has two conceptual layers: + +```text +trace extraction: + VulnerabilityWithInterproceduralTrace + -> relevant (MethodEntryPoint, TraceEntry) stream + +rule projection: + relevant TraceEntry stream + -> Map> +``` + +Keep these layers independently testable. The production implementation may +stream entries directly into the projector rather than retaining a large +intermediate list. + +Traversal completeness is defined structurally: + +```text +for every relevant FullStart2FinalTrace: + visit every element of entries + enqueue every relevant SummaryTrace referenced by those entries + +for every distinct enqueued SummaryTrace: + resolve every FullStart2FinalTrace + apply the same traversal +``` + +No source-to-sink path list or intra-method entry path is constructed. + +### 1. Validate and seed + +Create a per-invocation mutable map from rules to mutable action sets and seed +it with every vulnerability sink rule mapped to an empty set. + +Then classify the interprocedural trace: + +| input | result | +|---|---| +| `trace == null` | `Failed` | +| simple unconditional trace | `Collected(sink rule map)` | +| non-simple source-to-sink trace | continue | + +The simple case has no source-to-sink action trace to inspect. + +### 2. Build the relevant interprocedural corridor + +Handle a `SimpleTraceNode` before calling +`createSource2SinkGraph`, whose current contract expects interprocedural +roots. + +For a non-simple trace, call `createSource2SinkGraph` and compute nodes that +belong to at least one complete path without enumerating paths. + +First compute terminal reachability in reverse and retain only roots that can +reach both sides: + +```text +canReachSource = reverse reachability from sourceNodes +canReachSink = reverse reachability from sinkNodes +completeRoots = rootNodes intersect canReachSource intersect canReachSink +``` + +Then, for each direction, compute: + +```text +forwardReachable = nodes reachable from completeRoots +backwardReachable = canReachSource or canReachSink +corridor = forwardReachable intersect backwardReachable +``` + +Use the following adjacency: + +| direction | forward adjacency | backward adjacency | terminal set | +|---|---|---|---| +| root to source | `root2SourceFwd` | `root2SourceBwd` | `sourceNodes` | +| root to sink | `root2SinkFwd` | `root2SinkBwd` | `sinkNodes` | + +If `completeRoots` is empty, collection fails. Otherwise, the relevant +interprocedural node set is the union of the source and sink corridors. Each +retained action can therefore participate in at least one complete half-path, +and each retained root is connected to both a source and a sink. + +This union is required for sound staged rule selection. BaseOnly can expose +several shallow alternatives, including spurious ones. Selecting only the +first witness could collect rules for a spurious branch and omit the rules for +a real branch that Tree can reproduce in the full scan. + +At this point the corridor is a topological candidate corridor. Inner-summary +validity can still invalidate an action entry or an entire compact node. +Recompute the corridor after the dependency fixed point in step 5. + +### 3. Materialize trace models and discover dependencies + +For every compact interprocedural node in the corridor, use the same +operations as `TracePath.kt`: + +```text +InterProceduralStart2FinalTraceNode + -> resolveIntraProceduralFullStart2FinalTrace(Start2FinalTrace, ...) + +InterProceduralSummaryTraceNode + -> resolveIntraProceduralFullStart2FinalTrace(SummaryTrace, ...) +``` + +Resolution must run through `withMethodRunner(node.methodEntryPoint)`. Set +`collapseUnchangedNodes = true`; collapsing unchanged nodes preserves all +action entries and reduces memory. + +Traverse the complete `FullStart2FinalTrace.entries` array for every +materialized trace. Do not enumerate routes through `successors`. +`MethodTraceResolver` has already removed entries that are unreachable from +the selected start/final trace. The successor graph is used only for +reachability after an invalid summary dependency is pruned. + +Represent each returned full trace as a small dependency model: + +```text +ResolvedTraceModel: + entries + start ID + final ID + successors + optional inner SummaryTrace dependency per action entry +``` + +An entry has a dependency when its primary action is: + +- a `CallSummary` with a source edge; +- a `CallSummary` whose method edge changes the taint-mark set and whose + callee boundary is relevant; +- an internal `CallSourceSummary`. + +A mark-preserving `CallSummary` and an abstract-unmarked callee summary have +no dependency. + +Dependency extraction is context-sensitive for +`SourceStartEntry.sourcePrimaryAction`: + +| full-trace model origin | `SourceStartEntry.CallSourceSummary` | +|---|---| +| outer compact interprocedural node | represented by outer `CallToSource` edge; no local dependency | +| recursively discovered inner summary | required local `SummaryTrace` dependency | + +Discover dependencies with an invocation-local `SummaryTrace` worklist. +Resolve each distinct relevant summary key once with the strict +full-resolution API, traverse every entry of every returned full trace, and +store all returned full-trace models. Enqueue every relevant dependency found +in those entries. This discovery terminates on recursive call graphs because +keys are marked discovered before their full traces are inspected. + +`Cancelled` or `HardLimit` aborts the entire collection invocation with +`Failed`. Never convert a strict partial-resolution result to an invalid +summary model. Only `Complete(emptyList())` represents a semantically invalid +alternative that the fixed point may prune. + +Do not update the rule/action map during discovery. Some discovered traces and +entries may later prove to be dead alternatives. + +`InterProceduralSummaryTraceNode` should be supported by the materializer for +completeness, but current `TraceResolver` does not construct this node type at +runtime. + +### 4. Classify inner-summary validity by least fixed point + +Use the summary-boundary mark predicate above. Do not use the current default +`InnerCallTraceResolveStrategy` predicate: + +```text +SourceSummary -> true +MethodSummary -> edge.fact != edgeAfter.fact +``` + +The default answers whether a call changes an edge, not whether the callee +summary operates on a taint mark. + +Summary validity is a positive Boolean fixed point: + +```text +entryValid(E, V) = + E has no inner dependency + or dependency(E) is in valid-summary set V + +traceValid(T, V) = + T has a start-to-final path containing only entryValid entries + +summaryValid(S, V) = + any full trace of S satisfies traceValid(T, V) +``` + +Compute the least fixed point incrementally: + +```text +1. Build a reverse index: + dependency SummaryTrace -> dependent trace entries +2. Enable every entry with no dependency. +3. In each trace model, propagate reachability from its enabled start through + enabled entries. +4. When a trace final becomes reachable, mark its owning summary valid. +5. When a summary becomes valid, enable its dependent entries and continue + reachability propagation. +6. Stop when the worklist is empty. +``` + +This gives the required recursive semantics: + +- a non-recursive base path seeds validity; +- a recursive SCC with a path to a valid base becomes valid; +- a pure recursive SCC with no finite base path remains invalid. + +Merely marking a recursive summary “processed” is not enough: it would +incorrectly accept a cycle that has no finite trace. + +For any full trace and final valid-summary set, compute the relevant entry +corridor using only valid entries: + +```text +reachableFromStart(valid entries) + intersect +canReachFinal(valid entries) +``` + +An invalid alternative is pruned. It does not make a sibling valid alternative +fail, and its `otherActions` are not projected. + +### 5. Validate the outer graph and project relevant entries + +An outer compact node is valid when at least one of its full-trace models has +a valid start-to-final path under the final summary-validity set. + +Remove invalid compact nodes and their incident interprocedural edges, then +recompute `completeRoots`, root-to-source corridor, and root-to-sink corridor +as in step 2. If no complete root remains, return `Failed`. + +For every valid full trace of every node in the recomputed outer corridor, +iterate all entries and project each entry retained by its valid +start-to-final corridor. Then traverse every valid relevant inner summary +referenced by those entries, again iterating all of its full-trace entries. +Inner projection uses a visited-summary set only for deduplication; validity +has already been solved by the fixed point. + +Thus the algorithm traverses entries and summary graphs, not paths. The +reachability sets are Boolean filters over entries; they are never enumerated +as path sequences. + +For each projected entry: + +- inspect `Action.otherActions`; +- inspect `SourceStartEntry.sourceOtherActions`; +- ignore `Unchanged`, `Final`, `MethodEntry`, and structural primary actions. + +Ordering is not part of result equality. + +### 6. Freeze and return + +For each projected `RuleAction`, union all of its actions into the mutable set +stored under its rule. Preserve different rule objects that happen to share an +ID unless the rule configuration layer explicitly defines them as equal. + +Sink rules remain mapped to an empty set. Reject an attempt to add actions to +a sink-rule key or to register an action-owning rule as a sink. + +Create immutable snapshots of both the outer map and every inner action set, +then return `Collected(rules)`. + +## Required strict full-resolution status + +The current +`resolveIntraProceduralFullStart2FinalTrace` API returns a list even when its +`TraceBuilder` stopped because cancellation became inactive or the action hard +limit was reached. Such a list can be a partial trace. The action searcher +cannot distinguish it from a complete result and could incorrectly return a +partial `Collected`. + +Add a strict resolution API, or strengthen the existing one, to return: + +```kotlin +sealed interface FullTraceResolutionResult { + data class Complete( + val traces: List, + ) : FullTraceResolutionResult + + data object Cancelled : FullTraceResolutionResult + data object HardLimit : FullTraceResolutionResult +} +``` + +`TraceBuilder.resolveTrace` must report why its worklist loop stopped: + +- empty worklist -> complete; +- inactive cancellation -> cancelled; +- action limit -> hard limit. + +An empty trace list from a completed resolution means that no matching full +trace exists. It makes that outer node or inner summary invalid. Collection +fails only if pruning invalid models leaves no complete outer path. + +The reporting path may retain a best-effort adapter if needed, but +`TraceActionSearcher` must use the strict result. + +## Model cleanup + +Introduce a common semantic interface for rule-bearing actions: + +```kotlin +sealed interface RuleAction : TraceEntryAction { + val rule: CommonTaintConfigurationItem + val action: Set +} + +sealed interface CallRuleAction : CallAction, RuleAction +``` + +Make `SequentialSourceRule` implement `RuleAction`; the three existing call +rule variants continue through `CallRuleAction`. Kotlin's read-only `Set` +covariance permits source actions to retain their narrower action element +types. + +Then the collector has one projection: + +```text +RuleAction(rule, actions) + -> result.getOrPut(rule, ::mutableSetOf).addAll(actions) +``` + +This is preferable to a type switch in `TraceActionSearcher`: adding another +rule-bearing action without implementing `RuleAction` becomes a model-level +review error instead of a silent collector omission. + +The map contract is: + +```kotlin +data class Collected( + val rules: Map>, +) : ActionableRulesCollectionResult +``` + +An empty value set identifies a sink rule. All other entries have non-empty +value sets. + +## Failure contract + +Return `Failed` for: + +- a missing interprocedural trace; +- a non-simple trace with no complete source-to-sink path; +- no complete outer path after invalid inner-summary alternatives are pruned; +- cancellation, a trace-resolution hard limit, or an unexpected trace-model + invariant violation. + +Do not return `Failed` merely because: + +- a `CallSummary` is unmarked; +- an entry has no rule-bearing other actions; +- a `CallSourceSummary` has no direct map contribution; +- an action was already present in the rule's action set. + +The current shallow-scan consumer drops `Failed` discoveries. Therefore +failure must never be converted to a partially collected result. + +## Downstream full-scan contract + +`Phase.FullScan` currently receives the per-vulnerability `Collected` values, +while the JVM and Go consumers are still TODO. They should merge all maps +globally before configuring the full scan: + +```text +for each (rule, actions): + if rule is absent: + copy actions + else: + union actions into the existing set +``` + +Map interpretation is exact: + +```text +sink rule -> emptySet() -> enable that sink rule +rule -> {A1, A2, ...} -> enable exactly those actions for that rule +``` + +An empty action set is not a wildcard. Source rules require non-empty sets. +Assert that no merge combines an empty sink value with a non-empty action +value for the same rule. + +The JVM and Go selected providers narrow source rules to the selected actions +and enable only selected sink rules, but keep all pass-through rules and +cleaners available. Narrowing pass-through rules from a shallow trace would be +unsound, so the collector does not record pass actions. Prescan-derived +`relevantRuleIds` selection remains in effect before this action-level +filtering. + +## Concurrency and lifetime + +Actionable-rule resolution processes vulnerabilities in parallel. All mutable +search state must be invocation-local: + +- rule-to-mutable-action-set map; +- discovered summary models; +- valid-summary fixed-point set; +- projection visited-summary set; +- interprocedural and intra-method reachability worklists. + +Method runners and their trace stores remain shared, read-only inputs under +the existing trace-resolution concurrency contract. Do not introduce a +global summary-resolution cache in the first implementation: it would need +publication, cancellation, and AP-manager lifetime rules that are unnecessary +for correctness. + +The returned map and its action sets must not expose mutable collector state. + +## Complexity + +With the incremental reverse-dependency worklist, and excluding the cost +inside `MethodTraceResolver`, collector-side traversal is: + +```text +O(source-to-sink graph nodes and edges + + all materialized full-trace entries and edges + + inner-summary dependency references) +``` + +`collapseUnchangedNodes = true` and per-invocation summary deduplication are +the primary cost controls. No Cartesian product of source-to-sink alternatives +is required. Each summary changes to valid at most once, each dependent entry +is enabled at most once, and each reachability edge is propagated at most +once. + +Full-trace materialization itself can explore action combinations not present +in the returned graph and is guarded by the resolver action hard limit. Its +cost must be measured separately with existing trace-resolver step counters. + +Useful counters are: + +- outer graph nodes retained and pruned; +- full traces materialized; +- action entries visited; +- mark-changing inner summaries resolved; +- abstract-unmarked inner summaries skipped; +- summary dependency cycles discovered; +- distinct rules and actions emitted; +- failure reason. + +## Rejected alternatives + +### Use `generateTracePath` with `limit = 1` + +This is attractive because it already materializes full traces, but it is an +underapproximation for staged rule selection. A BaseOnly-only shallow branch +can be selected while a different branch contains an action needed under a +rule by a real Tree/full-scan path. The graph-corridor union is linear and +avoids that omission without enumerating path combinations. + +### Collect every node reachable from a root + +Forward reachability alone includes dead source or sink branches. Intersecting +forward and backward reachability retains only nodes that can reach the +corresponding terminal. + +### Classify a call using only the callee boundary + +The callee boundary says whether a trace operates on relevant facts, but does +not say whether resolving it can add an actionable source rule. Caller-side +`summaryEdges` determine whether the call introduces or changes a taint mark; +the callee boundary is retained as a secondary relevance guard. + +### Classify a call using only `FactAp.isAbstract()` + +Abstractness and markedness are independent in the general AP contract. +Markedness must be checked first with `TaintMarkAccessor`; abstractness is +then used only to recognize the explicitly skippable all-abstract/unmarked +case. + +### Apply a fixed inner-summary depth limit + +A fixed limit terminates recursion by silently omitting deeper rules or +actions. Deduplicating `SummaryTrace` keys terminates dependency discovery; +the least-fixed-point validity solver then preserves finite-path semantics +and rejects recursive SCCs without a valid base route. + +## Verification plan + +### Fact and summary classification + +Test the mark predicate independently for Tree and BaseOnly facts: + +- mark only on `SourceTraceEdge.fact`; +- mark only on `MethodTraceEdge.initialFact`; +- mark only on `MethodTraceEdge.fact`; +- mark on one `MethodTraceNDEdge.initialFacts` member; +- mark only on the ND output fact; +- abstract fact with a mark is relevant; +- abstract fact without a mark is irrelevant; +- method summary with equal non-empty before/after mark sets is irrelevant; +- method summary with different before/after mark sets is relevant; +- source summary is relevant regardless of equality; +- caller-side mark change with a callee final boundary that is entirely + abstract and unmarked is irrelevant. + +The last case pins the two-stage caller-transition and callee-boundary check. + +### Entry projection + +Test: + +- every vulnerability sink rule maps to `emptySet()`; +- one other action with multiple actions produces one rule key with all + actions; +- repeated actions deduplicate within the rule's action set; +- repeated uses of the same rule union their different actions; +- all four current rule-bearing other-action variants; +- source rules in `SourceStartEntry.sourceOtherActions`; +- structural primary actions add no map contribution; +- sink/action key collisions fail the representation invariant; +- map equality is independent of insertion and hash-iteration order. + +### Trace scenarios + +Add small dataflow samples for: + +1. a simple unconditional vulnerability: sink rule only; +2. sequential source -> pass rule -> sink, where the pass rule is not present + in the collected result; +3. source in a callee represented by `CallSourceSummary`: callee source rule is + obtained from the interprocedural source path; +4. mark-changing `CallSummary`: its inner source rule and action are collected; +5. unmarked abstract `CallSummary`: inner trace is not resolved and its rules + are not collected; +6. mark-changing inner summary with an unresolvable first route and a valid second + route: rules and actions from the valid resolved route are retained; +7. recursive mark-changing summary: collection terminates and returns each source rule with + its complete deduplicated action set; +8. missing trace and fully unresolvable mark-changing summary: `Failed`; +9. merged vulnerability sink rules: all sink keys are retained; +10. alternate source and sink branches: collect the union from every branch + in the complete-path corridor, but not from dead branches; +11. `CallSourceSummary` retained in an ordinary `Action` because of unchanged + edges or a non-source sibling action: resolve and collect its inner source + rule; +12. pure recursive inner-summary SCC: it is invalid without a finite base + path and becomes valid when a base alternative is added; +13. mark-changing `CallSummary` -> inner `SourceStartEntry.CallSourceSummary` -> + deeper source: collect the deeper source rule and invalidate the route if + the deeper summary has no finite trace; +14. cancellation and action-hard-limit exits after partial graph construction: + return `Failed`, never `Collected`. + +Each scenario should assert the exact rule-to-action-set map, not only success. + +### Integration + +Run a staged JVM and Go analysis where the full-scan rule provider is filtered +by the collected rule/action maps. Assert: + +- a true shallow branch is reproducible by the full scan; +- a shallow BaseOnly-only false discovery can disappear in the full scan; +- an unmarked structural helper does not cause unrelated rules to be enabled; +- Tree and BaseOnly collect compatible rule/action supersets for equivalent + semantic trace graphs. + +### Regression gates + +Run the full dataflow and both query-language suites. Add a workload with +nested and recursive summaries and assert structural counters rather than +wall-clock timing: + +- topologically dead outer branches are pruned before full-trace resolution; +- each distinct expanded `SummaryTrace` is fully resolved at most once per + vulnerability; +- abstract-unmarked summaries cause no inner full-trace resolution. + +## Implementation sequence + +1. Add `RuleAction` and the summary-boundary mark helpers with unit tests. +2. Add the strict full-trace resolution result and partial-resolution tests. +3. Extract/reuse source-to-sink graph construction and add corridor + reachability tests. +4. Implement dependency discovery, least-fixed-point validity, and valid + full-trace corridor traversal. +5. Implement `collectActionableRules` failure, simple-trace, graph-validity, + and projection handling. +6. Add end-to-end staged-analysis tests for JVM and Go. +7. Add counters and rerun the full dataflow/query-language test suites. + +## Acceptance criteria + +The feature is complete when: + +1. every `Collected` result comes from a resolved source-to-sink graph with at + least one complete source-to-sink path; +2. it contains every vulnerability sink rule and every source action grouped + under its rule from every relevant graph branch, including mark-changing + inner summaries; +3. it contains no rule or action solely from an abstract-unmarked inner + summary; +4. recursive summaries terminate without a semantic depth cutoff; +5. cancellation or a hard-limit partial resolution returns `Failed`, while a + semantically invalid alternative is pruned; +6. full scan configured from the collected maps does not lose a real branch + merely because BaseOnly also exposed a different shallow alternative; +7. JVM, Go, dataflow, and query-language regression suites remain green.