diff --git a/core/opentaint-configuration-rules/configuration-rules-common/src/main/kotlin/org/opentaint/dataflow/configuration/TaintCleanReach.kt b/core/opentaint-configuration-rules/configuration-rules-common/src/main/kotlin/org/opentaint/dataflow/configuration/TaintCleanReach.kt new file mode 100644 index 000000000..d812ac8da --- /dev/null +++ b/core/opentaint-configuration-rules/configuration-rules-common/src/main/kotlin/org/opentaint/dataflow/configuration/TaintCleanReach.kt @@ -0,0 +1,6 @@ +package org.opentaint.dataflow.configuration + +enum class TaintCleanReach { + Exact, + ExactAndAnyField, +} diff --git a/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt b/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt index e48f7baa4..76cc93e2e 100644 --- a/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt +++ b/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt @@ -31,10 +31,25 @@ sealed interface GoSerializedAssignAction : GoSerializedAction { } } -data class GoSerializedCleanAction( - val taintKind: String? = null, - val pos: PositionBaseWithModifiers, -) : GoSerializedAction +sealed interface GoSerializedCleanAction : GoSerializedAction { + val taintKind: String? + val pos: PositionBaseWithModifiers + + data class Direct( + override val taintKind: String? = null, + override val pos: PositionBaseWithModifiers, + ) : GoSerializedCleanAction + + data class AnyAccessor( + override val taintKind: String? = null, + override val pos: PositionBaseWithModifiers, + ) : GoSerializedCleanAction + + companion object { + operator fun invoke(taintKind: String? = null, pos: PositionBaseWithModifiers): GoSerializedCleanAction = + Direct(taintKind, pos) + } +} data class GoSerializedPassAction( val taintKind: String? = null, diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt index bff66b8b3..d64749b3e 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt @@ -2,6 +2,7 @@ package org.opentaint.dataflow.configuration.jvm import org.opentaint.dataflow.configuration.CommonTaintAction import org.opentaint.dataflow.configuration.CommonTaintAssignAction +import org.opentaint.dataflow.configuration.TaintCleanReach sealed interface Action: CommonTaintAction @@ -28,4 +29,5 @@ data class RemoveAllMarks( data class RemoveMark( val mark: TaintMark, val position: Position, + val reach: TaintCleanReach = TaintCleanReach.Exact, ) : Action diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt index f76412aea..04e73ce3c 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt @@ -1,6 +1,7 @@ package org.opentaint.dataflow.configuration.jvm.serialized import kotlinx.serialization.Serializable +import org.opentaint.dataflow.configuration.TaintCleanReach sealed interface SerializedAction @@ -15,6 +16,7 @@ data class SerializedTaintAssignAction( data class SerializedTaintCleanAction( val taintKind: String? = null, val pos: PositionBaseWithModifiers, + val reach: TaintCleanReach = TaintCleanReach.Exact, ): SerializedAction @Serializable diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedCondition.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedCondition.kt index f0a4e61df..062ab5783 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedCondition.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedCondition.kt @@ -6,6 +6,7 @@ import com.charleskorn.kaml.YamlNode import com.charleskorn.kaml.YamlScalar import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor @@ -141,6 +142,13 @@ sealed interface SerializedCondition { val pos: PositionBaseWithModifiers, ): SerializedCondition + @Serializable + data class ContainsMarkOnAnyField( + @SerialName("taintedOnAnyField") + val tainted: String, + val pos: PositionBaseWithModifiers, + ): SerializedCondition + @Serializable data class NumberOfArgs(val numberOfArgs: Int): SerializedCondition @@ -223,6 +231,7 @@ class SerializedConditionSerializer : companion object { private val serializerByProperty = mapOf( "tainted" to SerializedCondition.ContainsMark.serializer(), + "taintedOnAnyField" to SerializedCondition.ContainsMarkOnAnyField.serializer(), "anyOf" to SerializedCondition.Or.serializer(), "allOf" to SerializedCondition.And.serializer(), "not" to SerializedCondition.Not.serializer(), 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..162c0f3aa 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 @@ -10,7 +10,6 @@ import org.opentaint.dataflow.ap.ifds.MethodAnalyzer.FactToFactSub import org.opentaint.dataflow.ap.ifds.MethodAnalyzer.MethodCallHandler import org.opentaint.dataflow.ap.ifds.MethodAnalyzer.MethodCallResolutionFailureHandler import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication -import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp @@ -1242,7 +1241,10 @@ class NormalMethodAnalyzer( ndSummaryInitial.isEmpty() -> { summaryHandler.handleZeroToFact( currentEdgeFactAp, - SummaryExclusionRefinement(ExclusionSet.Universe), + SummaryEdgeApplication( + accessDelta = null, + initialFactExclusions = ExclusionSet.Universe, + ), summaryEdge.summaryEdge() ) } @@ -1252,7 +1254,10 @@ class NormalMethodAnalyzer( summaryHandler.handleFactToFact( initialFact, currentEdgeFactAp, - SummaryExclusionRefinement(initialFact.exclusions), + SummaryEdgeApplication( + accessDelta = null, + initialFactExclusions = initialFact.exclusions, + ), summaryEdge.summaryEdge() ) } @@ -1261,7 +1266,10 @@ class NormalMethodAnalyzer( summaryHandler.handleNDFactToFact( ndSummaryInitial, currentEdgeFactAp, - SummaryExclusionRefinement(ExclusionSet.Universe), + SummaryEdgeApplication( + accessDelta = null, + initialFactExclusions = ExclusionSet.Universe, + ), summaryEdge.summaryEdge() ) } @@ -1275,7 +1283,10 @@ class NormalMethodAnalyzer( summaryHandler.handleFactToFact( currentEdge.initialFactAp, currentEdgeFactAp, - SummaryExclusionRefinement(currentEdge.initialFactAp.exclusions), + SummaryEdgeApplication( + accessDelta = null, + initialFactExclusions = currentEdge.initialFactAp.exclusions, + ), summaryEdge.summaryEdge() ) } @@ -1284,7 +1295,10 @@ class NormalMethodAnalyzer( summaryHandler.handleNDFactToFact( ndSummaryInitial, currentEdgeFactAp, - SummaryExclusionRefinement(ExclusionSet.Universe), + SummaryEdgeApplication( + accessDelta = null, + initialFactExclusions = ExclusionSet.Universe, + ), summaryEdge.summaryEdge() ) } @@ -1295,7 +1309,10 @@ class NormalMethodAnalyzer( summaryHandler.handleNDFactToFact( ndSummaryInitial + currentEdge.initialFacts, currentEdgeFactAp, - SummaryExclusionRefinement(ExclusionSet.Universe), + SummaryEdgeApplication( + accessDelta = null, + initialFactExclusions = ExclusionSet.Universe, + ), summaryEdge.summaryEdge() ) } @@ -1882,4 +1899,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/MethodSummaryEdgeApplicationUtils.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummaryEdgeApplicationUtils.kt index 65ca456e1..5414ed3c9 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummaryEdgeApplicationUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummaryEdgeApplicationUtils.kt @@ -4,9 +4,20 @@ import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp object MethodSummaryEdgeApplicationUtils { - sealed interface SummaryEdgeApplication { - data class SummaryApRefinement(val delta: FinalFactAp.Delta) : SummaryEdgeApplication - data class SummaryExclusionRefinement(val exclusion: ExclusionSet) : SummaryEdgeApplication + /** + * The two independent refinements selected while matching a summary edge. + * + * [accessDelta] belongs to the access-path representation. [initialFactExclusions] belongs to + * demand analysis and is present only for an empty access delta. Synthetic ND applications + * may supply only [initialFactExclusions]. + */ + data class SummaryEdgeApplication( + val accessDelta: FinalFactAp.Delta?, + val initialFactExclusions: ExclusionSet?, + ) { + init { + require(accessDelta != null || initialFactExclusions != null) + } } fun tryApplySummaryEdge( @@ -15,11 +26,16 @@ object MethodSummaryEdgeApplicationUtils { ): List = methodInitialFactAp.delta(methodSummaryInitialFactAp).map { delta -> if (delta.isEmpty) { - SummaryEdgeApplication.SummaryExclusionRefinement( - methodInitialFactAp.exclusions.union(methodSummaryInitialFactAp.exclusions) + SummaryEdgeApplication( + accessDelta = delta, + initialFactExclusions = + methodInitialFactAp.exclusions.union(methodSummaryInitialFactAp.exclusions), ) } else { - SummaryEdgeApplication.SummaryApRefinement(delta) + SummaryEdgeApplication( + accessDelta = delta, + initialFactExclusions = null, + ) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/AnyFieldMarkExclusions.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/AnyFieldMarkExclusions.kt new file mode 100644 index 000000000..7879f1324 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/AnyFieldMarkExclusions.kt @@ -0,0 +1,160 @@ +package org.opentaint.dataflow.ap.ifds.access + +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx + +/** + * Marks excluded from future materialization of an AnyField abstraction. + * + * An AnyField cleaner removes every currently materialized matching mark and records here what + * must remain excluded if the fact later grows. Tree stores the value on abstract nodes; Automata + * and Cactus store it on their final access values. Initial facts never carry it. + * + * Each mark carries the minimum relative depth below the AnyField at which it is excluded: + * + * - [marksFromDepth1] applies to a direct mark child and everything deeper. + * - [marksFromDepth2] preserves a direct mark child and applies after one intervening accessor. + * + * Arrays are sorted and disjoint. [create] returns `null` for an empty tree annotation; root-only + * representations use [Empty] as their explicit neutral value. + */ +class AnyFieldMarkExclusions private constructor( + @JvmField val marksFromDepth1: IntArray, + @JvmField val marksFromDepth2: IntArray, +) { + private val hash: Int = marksFromDepth1.contentHashCode() * 31 + marksFromDepth2.contentHashCode() + + override fun hashCode(): Int = hash + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AnyFieldMarkExclusions) return false + if (hash != other.hash) return false + return marksFromDepth1.contentEquals(other.marksFromDepth1) + && marksFromDepth2.contentEquals(other.marksFromDepth2) + } + + operator fun contains(mark: AccessorIdx): Boolean = + marksFromDepth1.binarySearch(mark) >= 0 || marksFromDepth2.binarySearch(mark) >= 0 + + val isEmpty: Boolean + get() = marksFromDepth1.isEmpty() && marksFromDepth2.isEmpty() + + /** A base-level any-field clean starts applying below one concrete accessor. */ + fun add(mark: AccessorIdx): AnyFieldMarkExclusions = addMarkFromDepth2(mark) + + internal infix fun then(other: AnyFieldMarkExclusions): AnyFieldMarkExclusions = + then(this, other) ?: Empty + + internal infix fun join(other: AnyFieldMarkExclusions): AnyFieldMarkExclusions = + join(this, other) ?: Empty + + private fun allMarks(): IntArray = (marksFromDepth1 + marksFromDepth2).also { it.sort() } + + /** + * The claim as seen from any position at least one accessor below the annotated node: + * everything below such a position is at depth >= 2 relative to the annotated node, so every + * claimed mark — depth-1 and depth-2 alike — applies from relative depth 1 there. + */ + fun collapseToDepth1(): AnyFieldMarkExclusions = + if (marksFromDepth2.isEmpty()) this else AnyFieldMarkExclusions(allMarks(), EMPTY) + + override fun toString(): String = buildString { + append("!*{d1=") + append(marksFromDepth1.joinToString(",")) + append(";d2=") + append(marksFromDepth2.joinToString(",")) + append("}") + } + + companion object { + private val EMPTY = IntArray(0) + val Empty = AnyFieldMarkExclusions(EMPTY, EMPTY) + + /** + * [marksFromDepth1] and [marksFromDepth2] must each be sorted; a mark present in both is + * kept at depth 1. Alternative executions must instead combine through [join], which + * resolves the conflict in the weaker direction. + */ + fun create(marksFromDepth1: IntArray, marksFromDepth2: IntArray): AnyFieldMarkExclusions? { + val d2 = if (marksFromDepth2.any { marksFromDepth1.binarySearch(it) >= 0 }) { + marksFromDepth2.filter { marksFromDepth1.binarySearch(it) < 0 }.toIntArray() + } else { + marksFromDepth2 + } + + if (marksFromDepth1.isEmpty() && d2.isEmpty()) return null + return AnyFieldMarkExclusions(marksFromDepth1, d2) + } + + private fun fromDepth1(mark: AccessorIdx): AnyFieldMarkExclusions = AnyFieldMarkExclusions(intArrayOf(mark), EMPTY) + + private fun fromDepth2(mark: AccessorIdx): AnyFieldMarkExclusions = AnyFieldMarkExclusions(EMPTY, intArrayOf(mark)) + + fun AnyFieldMarkExclusions?.addMarkFromDepth1(mark: AccessorIdx): AnyFieldMarkExclusions { + if (this == null) return fromDepth1(mark) + if (marksFromDepth1.binarySearch(mark) >= 0) return this + // depth 1 is the stronger claim: it absorbs a depth-2 entry for the same mark + val d1 = (marksFromDepth1 + mark).also { it.sort() } + val d2 = if (marksFromDepth2.binarySearch(mark) >= 0) { + marksFromDepth2.filter { it != mark }.toIntArray() + } else { + marksFromDepth2 + } + return AnyFieldMarkExclusions(d1, d2) + } + + fun AnyFieldMarkExclusions?.addMarkFromDepth2(mark: AccessorIdx): AnyFieldMarkExclusions { + if (this == null) return fromDepth2(mark) + if (contains(mark)) return this + val d2 = (marksFromDepth2 + mark).also { it.sort() } + return AnyFieldMarkExclusions(marksFromDepth1, d2) + } + + /** + * The join of two alternative executions meeting at the SAME abstract node: a mark + * survives only when both alternatives exclude it, and at the weaker of the two depths + * (max — a claim both alternatives make only from depth 2 cannot be strengthened to + * depth 1). + * + * The abstraction state itself joins with "not abstract" as the identity: when only one + * operand is abstract, all abstraction (and its annotation) comes from that operand — + * callers handle that case and reach here only with two abstract operands. + */ + fun join(a: AnyFieldMarkExclusions?, b: AnyFieldMarkExclusions?): AnyFieldMarkExclusions? { + if (a == null || b == null) return null + if (a == b) return a + + val d1 = a.marksFromDepth1.filter { b.marksFromDepth1.binarySearch(it) >= 0 }.toIntArray() + val d2 = mutableListOf() + for (mark in a.allMarks()) { + if (d1.binarySearch(mark) >= 0) continue + if (b.contains(mark)) d2.add(mark) + } + return create(d1, d2.toIntArray()) + } + + /** + * Sequential composition of two claims that BOTH hold: the caller had already cleaned one + * mark when the callee's summary, whose exit abstraction continues the same object, + * cleaned another. Marks union; a mark claimed at both depths keeps the stronger (min — + * depth 1 covers everything depth 2 does). + */ + fun then(a: AnyFieldMarkExclusions?, b: AnyFieldMarkExclusions?): AnyFieldMarkExclusions? { + if (a == null) return b + if (b == null) return a + if (a == b) return a + + val d1 = (a.marksFromDepth1.toSet() + b.marksFromDepth1.toSet()).toIntArray().also { it.sort() } + val d2 = (a.marksFromDepth2.toSet() + b.marksFromDepth2.toSet()) + .filter { d1.binarySearch(it) < 0 } + .toIntArray().also { it.sort() } + return create(d1, d2) + } + } +} + +internal fun AnyFieldMarkExclusions.forExclusions( + exclusions: ExclusionSet, +): AnyFieldMarkExclusions = + if (exclusions is ExclusionSet.Universe) AnyFieldMarkExclusions.Empty else this diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt index 7c2899889..8ed334811 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt @@ -2,8 +2,11 @@ package org.opentaint.dataflow.ap.ifds.access 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.TaintMarkAccessor +import org.opentaint.dataflow.taint.Cleaner interface AccessorList { fun startsWithAccessor(accessor: Accessor): Boolean @@ -57,6 +60,14 @@ interface FinalFactAp : FactAp, ReadableAccessorList { fun removeAbstraction(): FinalFactAp? fun abstractOnly(): FinalFactAp + /** + * The dual of [removeAbstraction]: the fact reduced to its root abstraction — no concrete + * children, but all representation state attached to the abstraction preserved. Callers + * partitioning an abstract fact must use this rather than rebuilding a bare abstraction. + * Only meaningful when [isAbstract] is true. + */ + fun abstractPart(): FinalFactAp + interface Delta: ReadableAccessorList { val isEmpty: Boolean } @@ -72,4 +83,39 @@ interface FinalFactAp : FactAp, ReadableAccessorList { fun hasEmptyDelta(other: InitialFactAp): Boolean = delta(other).any { it.isEmpty } + + /** + * Applies one cleaner position to this fact. + * + * A concrete position is removed directly. If the position crosses an AnyField, the + * representation also records the matching mark exclusion for content materialized later. + * Callers do not distinguish those cases. + */ + fun clean(cleaner: Cleaner): CleanResult + + /** + * Removes a mark from both the exact position and its currently represented AnyField + * alternative, without excluding the mark from future AnyField growth. + */ + fun cleanExactAndAnyField(mark: TaintMarkAccessor): CleanResult { + val afterAny = readAccessor(AnyAccessor) + ?: error("Fact reports an any-field accessor but cannot read it") + + val clearedAfterAny = afterAny.clearAccessor(mark) + val restoredAfterAny = clearedAfterAny?.prependAccessor(AnyAccessor) + + val withoutAny = clearAccessor(AnyAccessor) + val cleanedWithoutAny = withoutAny?.clearAccessor(mark) + + val cleaned = clearedAfterAny != afterAny || cleanedWithoutAny != withoutAny + return CleanResult( + listOfNotNull(restoredAfterAny, cleanedWithoutAny), + removedAlternative = cleaned, + ) + } + + data class CleanResult( + val survivingFacts: List, + val removedAlternative: Boolean, + ) } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactCleaner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactCleaner.kt new file mode 100644 index 000000000..eb270bebc --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactCleaner.kt @@ -0,0 +1,75 @@ +package org.opentaint.dataflow.ap.ifds.access + +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.configuration.TaintCleanReach +import org.opentaint.dataflow.taint.Cleaner +import org.opentaint.dataflow.taint.accessors +import org.opentaint.dataflow.taint.base +import org.opentaint.dataflow.taint.removePrefix + +/** + * Representation-neutral traversal for concrete cleaner positions. + * + * Only the mark exclusion created by `[any].![mark]` is representation-specific, because it must + * survive future materialization of an abstract fact. + */ +internal fun FinalFactAp.clean( + cleaner: Cleaner, + cleanAnyField: (TaintMarkAccessor) -> FinalFactAp.CleanResult, +): FinalFactAp.CleanResult { + require(cleaner.position.base() == base) { "Cleaner and fact bases must match" } + + if (cleaner is Cleaner.Mark) { + val positionAccessors = cleaner.position.accessors() + if (positionAccessors.size == 1 && positionAccessors.single() is AnyAccessor) { + return cleanAnyField(cleaner.mark) + } + } + + return cleanConcrete(cleaner) +} + +private fun Cleaner.accessors(): List = + position.accessors() + if (this is Cleaner.Mark) listOf(mark) else emptyList() + +private fun FinalFactAp.cleanConcrete(cleaner: Cleaner): FinalFactAp.CleanResult { + val accessors = cleaner.accessors() + if (accessors.isEmpty()) { + check(cleaner is Cleaner.AllMarks) + return FinalFactAp.CleanResult(emptyList(), removedAlternative = true) + } + + val head = accessors.first() + val tail = accessors.drop(1) + if (tail.isEmpty()) { + if (cleaner is Cleaner.Mark && + cleaner.reach == TaintCleanReach.ExactAndAnyField && + startsWithAccessor(AnyAccessor) + ) { + return cleanExactAndAnyField(cleaner.mark) + } + + if (!startsWithAccessor(head)) { + return FinalFactAp.CleanResult(listOf(this), removedAlternative = false) + } + + val cleared = clearAccessor(head) + return FinalFactAp.CleanResult( + listOfNotNull(cleared), + removedAlternative = cleared != this, + ) + } + + val child = readAccessor(head) + ?: return FinalFactAp.CleanResult(listOf(this), removedAlternative = false) + + val remaining = listOfNotNull(clearAccessor(head)) + val cleanedChild = child.clean(cleaner.removePrefix(head)) + val restoredChildren = cleanedChild.survivingFacts.map { it.prependAccessor(head) } + return FinalFactAp.CleanResult( + remaining + restoredChildren, + removedAlternative = cleanedChild.removedAlternative, + ) +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt index 311d69569..e373659d2 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt @@ -10,6 +10,8 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet 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.AnyFieldMarkExclusions +import org.opentaint.dataflow.ap.ifds.access.forExclusions import org.opentaint.dataflow.ap.ifds.FactTypeChecker.CompatibilityFilterResult import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext @@ -79,12 +81,28 @@ class AccessGraph( private val edges: PersistentInt2LongMap, private val nodeSucc: Array, private val nodePred: Array, + val anyFieldMarkExclusions: AnyFieldMarkExclusions = AnyFieldMarkExclusions.Empty, ) { private val numNodes: Int get() = nodeSucc.size val size: Int get() = edges.size - private val hash: Int by lazy(LazyThreadSafetyMode.PUBLICATION) { dfsHash() } + private val hash: Int by lazy(LazyThreadSafetyMode.PUBLICATION) { + 31 * dfsHash() + anyFieldMarkExclusions.hashCode() + } + + fun withAnyFieldMarkExclusions(exclusions: AnyFieldMarkExclusions): AccessGraph = + if (exclusions === anyFieldMarkExclusions) { + this + } else { + AccessGraph(manager, initial, final, edges, nodeSucc, nodePred, exclusions) + } + + fun withoutAnyFieldMarkExclusions(): AccessGraph = + withAnyFieldMarkExclusions(AnyFieldMarkExclusions.Empty) + + fun forExclusions(exclusions: ExclusionSet): AccessGraph = + withAnyFieldMarkExclusions(anyFieldMarkExclusions.forExclusions(exclusions)) fun getAllOwnAccessors() = edges.keys.mapNotNullTo(hashSetOf()) { @@ -130,6 +148,7 @@ class AccessGraph( if (this === other) return true if (other !is AccessGraph) return false + if (anyFieldMarkExclusions != other.anyFieldMarkExclusions) return false if (edges.size != other.edges.size) return false if (edges.keys != other.edges.keys) return false @@ -277,7 +296,15 @@ class AccessGraph( } private fun create(initial: NodeMarker, final: NodeMarker): AccessGraph = - AccessGraph(manager, initial, final, edges, nodeSucc, nodePred) + AccessGraph( + manager, + initial, + final, + edges, + nodeSucc, + nodePred, + anyFieldMarkExclusions, + ) fun startsWith(accessor: AccessorIdx): Boolean = getStateSuccessorUnsafe(initial, accessor) != NO_NODE @@ -321,6 +348,46 @@ class AccessGraph( } } + fun enforceAnyFieldMarkExclusions( + exclusions: AnyFieldMarkExclusions, + keepInitialLevel: Boolean, + ): AccessGraph? { + if (exclusions.isEmpty) return this + + val depth1 = BitSet() + exclusions.marksFromDepth1.forEach(depth1::set) + val withoutDepth1 = removeDeepAccessors(depth1, keepInitialLevel = false) ?: return null + + val depth2 = BitSet() + exclusions.marksFromDepth2.forEach(depth2::set) + return withoutDepth1.removeDeepAccessors(depth2, keepInitialLevel) + } + + fun cleanAnyField(mark: AccessorIdx): AccessGraph? = + removeDeepAccessors(bitSetOf(mark), keepInitialLevel = true) + + fun cleanExactAndAnyField(mark: AccessorIdx): AccessGraph? = + removeDeepAccessors(bitSetOf(mark), keepInitialLevel = false) + + private fun removeDeepAccessors(deepAccessors: BitSet, keepInitialLevel: Boolean): AccessGraph? { + val keepAtInitial = keepInitialLevel && nodePred[initial].let { it == null || it.isEmpty } + + val edgesToRemove = BitSet() + deepAccessors.forEach { accessor -> + val edge = edges.get(accessor) + if (edge == NO_EDGE) return@forEach + if (keepAtInitial && edge.from == initial) return@forEach + edgesToRemove.set(accessor) + } + + if (edgesToRemove.isEmpty) return this + + return mutable() + .removeEdges(edgesToRemove) + .persist() + .removeUnreachableNodes() + } + private fun filter(exclusionSet: BitSet): AccessGraph? { val mutableCopy = mutable() val mutableResult = mutableCopy.clear(exclusionSet) ?: return null @@ -334,10 +401,13 @@ class AccessGraph( fun concat(other: AccessGraph): AccessGraph { val mutableCopy = mutable() val mutableResult = mutableCopy.concat(other) + val composedExclusions = anyFieldMarkExclusions then other.anyFieldMarkExclusions - if (mutableResult === mutableCopy) return this + if (mutableResult === mutableCopy) { + return withAnyFieldMarkExclusions(composedExclusions) + } - return mutableResult.persist() + return mutableResult.persist().withAnyFieldMarkExclusions(composedExclusions) } fun delta(other: AccessGraph): List { @@ -356,13 +426,25 @@ class AccessGraph( private fun splitOutEmptyDelta(delta: AccessGraph): List { if (delta.initialNodeIsFinal() && !delta.isEmpty()) { - return listOf(delta, manager.emptyGraph()) + return listOf( + delta, + manager.emptyGraph().withAnyFieldMarkExclusions(anyFieldMarkExclusions), + ) } return listOf(delta) } fun containsAll(other: AccessGraph): Boolean { + if ((anyFieldMarkExclusions join other.anyFieldMarkExclusions) != + anyFieldMarkExclusions + ) { + return false + } + return containsAllAccessPaths(other) + } + + fun containsAllAccessPaths(other: AccessGraph): Boolean { if (other.isEmpty()) return this.initial == this.final if (this.isEmpty()) return false @@ -428,7 +510,7 @@ class AccessGraph( .removeUnreachableNodes() ?: return@forEach - if (!other.containsAll(matchedPrefix)) return@forEach + if (!other.containsAllAccessPaths(matchedPrefix)) return@forEach val deltaSuffix = AccessGraph(manager, splitNode, final, edges, nodeSucc, nodePred) .removeUnreachableNodes() @@ -472,14 +554,19 @@ class AccessGraph( fun merge(other: AccessGraph): AccessGraph { check(manager === other.manager) + if (this == other) return this val mutableCopy = mutable() val mergedMutable = mutableCopy.merge(other) - val mergeResult = mergedMutable.persist() + val mergeResult = mergedMutable.persist().withAnyFieldMarkExclusions( + anyFieldMarkExclusions join other.anyFieldMarkExclusions + ) return mergeResult } fun filter(filter: FactTypeChecker.FactCompatibilityFilter): AccessGraph? { + if (isEmpty() || filter === FactTypeChecker.AlwaysCompatibleFilter) return this + val rejectedPredecessors = BitSet() val finalPredecessors = nodePredecessors(final) finalPredecessors.forEach { accessor -> @@ -612,6 +699,7 @@ class AccessGraph( edges, edges.mutable(), PersistentArrayBuilder(nodeSucc), PersistentArrayBuilder(nodePred), + anyFieldMarkExclusions, ) internal class Serializer( @@ -719,6 +807,7 @@ class MutableAccessGraph( private val mutableEdges: PersistentInt2LongMap, private val nodeSucc: PersistentArrayBuilder, private val nodePred: PersistentArrayBuilder, + private val anyFieldMarkExclusions: AnyFieldMarkExclusions, ) { private val numNodes: Int get() = nodeSucc.size @@ -729,7 +818,8 @@ class MutableAccessGraph( manager, initial, final, originalPersistentEdges, mutableEdges, - nodeSucc, nodePred + nodeSucc, nodePred, + anyFieldMarkExclusions, ) fun persist(): AccessGraph = AccessGraph( @@ -737,7 +827,8 @@ class MutableAccessGraph( initial, final, mutableEdges.persist(originalPersistentEdges), nodeSucc.persist(), - nodePred.persist() + nodePred.persist(), + anyFieldMarkExclusions, ) fun prepend(accessor: AccessorIdx): MutableAccessGraph { @@ -866,6 +957,18 @@ class MutableAccessGraph( return freshNode } + fun removeEdges(accessors: BitSet): MutableAccessGraph { + accessors.forEach { accessor -> + val edge = removeEdge(accessor) + check(edge != NO_EDGE) { "No edge" } + + removeNodeSuccessor(edge.from, accessor) + removeNodePredecessor(edge.to, accessor) + } + + return create(initial, final) + } + fun clear(accessors: BitSet): MutableAccessGraph? { val initialSuccessors = nodeSuccessors(initial) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphApSerializer.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphApSerializer.kt index b06850f95..fb6c9308c 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphApSerializer.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphApSerializer.kt @@ -1,12 +1,14 @@ package org.opentaint.dataflow.ap.ifds.access.automata 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.ExclusionSet +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions import org.opentaint.dataflow.ap.ifds.access.InitialFactAp 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.AnyFieldMarkExclusionsSerializer import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext import java.io.DataInputStream import java.io.DataOutputStream @@ -16,48 +18,80 @@ internal class AccessGraphApSerializer( context: SummarySerializationContext ) : ApSerializer { private val accessGraphSerializer = AccessGraph.Serializer(manager, context) - private val exclusionSetSerializer = ExclusionSetSerializer(context) + private val exclusionSerializer = ExclusionSetSerializer(context) + private val anyFieldMarkExclusionsSerializer = with(manager) { + AnyFieldMarkExclusionsSerializer(context, { it.idx }, { it.accessor }) + } - private fun DataOutputStream.writeAp(base: AccessPathBase, access: AccessGraph, exclusions: ExclusionSet) { + private fun DataOutputStream.writeInitialApFields( + base: AccessPathBase, + access: AccessGraph, + exclusion: ExclusionSet, + ) { with (AccessPathBaseSerializer) { writeAccessPathBase(base) } - with (exclusionSetSerializer) { - writeExclusionSet(exclusions) + with (exclusionSerializer) { + writeExclusionSet(exclusion) } with (accessGraphSerializer) { writeGraph(access) } } - private fun DataInputStream.readAp(builder: (AccessPathBase, AccessGraph, ExclusionSet) -> T): T { + private fun DataInputStream.readInitialApFields( + builder: (AccessPathBase, AccessGraph, ExclusionSet) -> T, + ): T { val base = with (AccessPathBaseSerializer) { readAccessPathBase() } - val exclusions = with (exclusionSetSerializer) { + val exclusion = with (exclusionSerializer) { readExclusionSet() } val access = with (accessGraphSerializer) { readGraph() } - return builder(base, access, exclusions) + return builder(base, access, exclusion) } override fun DataOutputStream.writeFinalAp(ap: FinalFactAp) { (ap as AccessGraphFinalFactAp) - writeAp(ap.base, ap.access, ap.exclusions) + with (AccessPathBaseSerializer) { + writeAccessPathBase(ap.base) + } + with (exclusionSerializer) { + writeExclusionSet(ap.exclusions) + } + with(anyFieldMarkExclusionsSerializer) { + writeAnyFieldMarkExclusions(ap.anyFieldMarkExclusions) + } + with (accessGraphSerializer) { + writeGraph(ap.access) + } } override fun DataOutputStream.writeInitialAp(ap: InitialFactAp) { (ap as AccessGraphInitialFactAp) - writeAp(ap.base, ap.access, ap.exclusions) + writeInitialApFields(ap.base, ap.access, ap.exclusions) } override fun DataInputStream.readFinalAp(): FinalFactAp { - return readAp(::AccessGraphFinalFactAp) + val base = with(AccessPathBaseSerializer) { readAccessPathBase() } + val exclusions = with(exclusionSerializer) { readExclusionSet() } + val anyFieldMarkExclusions = with(anyFieldMarkExclusionsSerializer) { + readAnyFieldMarkExclusions() + } + val access = with(accessGraphSerializer) { readGraph() } + return AccessGraphFinalFactAp( + base, + access.withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusions, + ) } override fun DataInputStream.readInitialAp(): InitialFactAp { - return readAp(::AccessGraphInitialFactAp) + return readInitialApFields { base, access, exclusions -> + AccessGraphInitialFactAp(base, access, exclusions) + } } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt index fc7637b7c..ab8031ae5 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt @@ -5,15 +5,29 @@ 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.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.clean +import org.opentaint.dataflow.taint.Cleaner +import org.opentaint.dataflow.ap.ifds.access.forExclusions import org.opentaint.dataflow.ap.ifds.tryAnyAccessorOrNull data class AccessGraphFinalFactAp( override val base: AccessPathBase, override val access: AccessGraph, - override val exclusions: ExclusionSet + override val exclusions: ExclusionSet, ) : FinalFactAp, AccessGraphAccessorList { + val anyFieldMarkExclusions: AnyFieldMarkExclusions + get() = access.anyFieldMarkExclusions + + init { + check(exclusions !is ExclusionSet.Universe || anyFieldMarkExclusions.isEmpty) { + "Universe facts cannot carry AnyField mark exclusions" + } + } + override val size: Int get() = access.size override val depth: Int get() = size @@ -26,7 +40,15 @@ data class AccessGraphFinalFactAp( } override fun replaceExclusions(exclusions: ExclusionSet): FinalFactAp = - AccessGraphFinalFactAp(base, access, exclusions) + AccessGraphFinalFactAp(base, access.forExclusions(exclusions), exclusions) + + // Cleaner state belongs to the graph value even when its access-path shape is empty. + override fun abstractPart(): FinalFactAp = + AccessGraphFinalFactAp( + base, + access.manager.emptyGraph().withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusions, + ) override fun isAbstract(): Boolean = exclusions !is ExclusionSet.Universe && access.initialNodeIsFinal() @@ -46,6 +68,43 @@ data class AccessGraphFinalFactAp( return access.clear(accessor.idx)?.let { AccessGraphFinalFactAp(base, it, exclusions) } } + override fun clean(cleaner: Cleaner): FinalFactAp.CleanResult = + clean(cleaner, ::cleanAnyField) + + override fun cleanExactAndAnyField( + mark: TaintMarkAccessor, + ): FinalFactAp.CleanResult { + val cleaned = with(access.manager) { access.cleanExactAndAnyField(mark.idx) } + ?: return FinalFactAp.CleanResult(emptyList(), removedAlternative = true) + if (cleaned === access) { + return FinalFactAp.CleanResult(listOf(this), removedAlternative = false) + } + return FinalFactAp.CleanResult( + listOf(AccessGraphFinalFactAp(base, cleaned, exclusions)), + removedAlternative = true, + ) + } + + private fun cleanAnyField( + mark: TaintMarkAccessor, + ): FinalFactAp.CleanResult { + val cleaned = with(access.manager) { access.cleanAnyField(mark.idx) } + ?: return FinalFactAp.CleanResult(emptyList(), removedAlternative = true) + val cleanedAnyFieldMarkExclusions = with(access.manager) { + anyFieldMarkExclusions.add(mark.idx) + }.forExclusions(exclusions) + return FinalFactAp.CleanResult( + survivingFacts = listOf( + AccessGraphFinalFactAp( + base, + cleaned.withAnyFieldMarkExclusions(cleanedAnyFieldMarkExclusions), + exclusions, + ) + ), + removedAlternative = false, + ) + } + override fun removeAbstraction(): FinalFactAp? { /** * Automata is at an abstraction point when its @@ -59,14 +118,19 @@ data class AccessGraphFinalFactAp( override fun abstractOnly(): FinalFactAp = AccessGraphFinalFactAp(base, access.manager.emptyGraph(), exclusions) - data class Delta(override val access: AccessGraph) : FinalFactAp.Delta, AccessGraphAccessorList { + data class Delta( + override val access: AccessGraph, + ) : FinalFactAp.Delta, AccessGraphAccessorList { + val anyFieldMarkExclusions: AnyFieldMarkExclusions + get() = access.anyFieldMarkExclusions + override val isEmpty: Boolean get() = access.isEmpty() override fun readAccessor(accessor: Accessor): FinalFactAp.Delta? = with(access.manager) { val newGraph = access.read(accessor.idx) ?: tryAnyAccessorOrNull(accessor) { access.read(anyAccessorIdx) } - return newGraph?.let { Delta(it) } + return newGraph?.let(::Delta) } override fun isAbstract(): Boolean = access.initialNodeIsFinal() @@ -77,8 +141,14 @@ data class AccessGraphFinalFactAp( if (base != other.base) return emptyList() return access.delta(other.access).mapNotNull { delta -> - val filteredDelta = delta.filter(other.exclusions) - filteredDelta?.let { Delta(it) } + val filteredDelta = delta + .filter(other.exclusions) + ?.enforceAnyFieldMarkExclusions( + anyFieldMarkExclusions, + keepInitialLevel = other.access.isEmpty(), + ) + ?: return@mapNotNull null + Delta(filteredDelta.withAnyFieldMarkExclusions(anyFieldMarkExclusions)) } } @@ -86,22 +156,38 @@ data class AccessGraphFinalFactAp( other as AccessGraphInitialFactAp if (base != other.base) return false - return access.containsAll(other.access) + return access.containsAllAccessPaths(other.access) } override fun concat(typeChecker: FactTypeChecker, delta: FinalFactAp.Delta): FinalFactAp? { - if (delta.isEmpty) return this delta as Delta + val composedAnyFieldMarkExclusions = (anyFieldMarkExclusions then delta.anyFieldMarkExclusions) + .forExclusions(exclusions) + if (delta.isEmpty) { + return AccessGraphFinalFactAp( + base, + access.withAnyFieldMarkExclusions(composedAnyFieldMarkExclusions), + exclusions, + ) + } val filter = access.manager.createFilter(access, typeChecker) val filteredDelta = delta.access.filter(filter) ?: return null if (access.isEmpty()) { - return AccessGraphFinalFactAp(base, filteredDelta, exclusions) + return AccessGraphFinalFactAp( + base, + filteredDelta.withAnyFieldMarkExclusions(composedAnyFieldMarkExclusions), + exclusions, + ) } val concatenatedGraph = access.concat(filteredDelta) - return AccessGraphFinalFactAp(base, concatenatedGraph, exclusions) + return AccessGraphFinalFactAp( + base, + concatenatedGraph.withAnyFieldMarkExclusions(composedAnyFieldMarkExclusions), + exclusions, + ) } override fun filterFact(filter: FactTypeChecker.FactApFilter): FinalFactAp? = @@ -114,7 +200,7 @@ data class AccessGraphFinalFactAp( factAp as AccessGraphInitialFactAp if (base != factAp.base) return false - return access.containsAll(factAp.access) + return access.containsAllAccessPaths(factAp.access) } override fun equalTo(factAp: InitialFactAp): Boolean { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt index f78910922..9fb6f7ccb 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt @@ -5,6 +5,7 @@ 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.access.AnyFieldMarkExclusions import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp @@ -13,6 +14,12 @@ data class AccessGraphInitialFactAp( override val access: AccessGraph, override val exclusions: ExclusionSet, ) : InitialFactAp, AccessGraphAccessorList { + init { + check(access.anyFieldMarkExclusions.isEmpty) { + "Initial facts cannot carry AnyField mark exclusions" + } + } + override val size: Int get() = access.size override val depth: Int get() = size @@ -32,7 +39,9 @@ data class AccessGraphInitialFactAp( override fun readAccessor(accessor: Accessor): InitialFactAp? = with(access.manager) { check(accessor !is AnyAccessor) - return access.read(accessor.idx)?.let { AccessGraphInitialFactAp(base, it, exclusions) } + return access.read(accessor.idx)?.let { + AccessGraphInitialFactAp(base, it, exclusions) + } } override fun prependAccessor(accessor: Accessor): InitialFactAp = with(access.manager) { @@ -42,10 +51,17 @@ data class AccessGraphInitialFactAp( override fun clearAccessor(accessor: Accessor): InitialFactAp? = with(access.manager) { check(accessor !is AnyAccessor) - return access.clear(accessor.idx)?.let { AccessGraphInitialFactAp(base, it, exclusions) } + return access.clear(accessor.idx)?.let { + AccessGraphInitialFactAp(base, it, exclusions) + } } - data class Delta(override val access: AccessGraph) : InitialFactAp.Delta, AccessGraphAccessorList { + data class Delta( + override val access: AccessGraph, + ) : InitialFactAp.Delta, AccessGraphAccessorList { + val anyFieldMarkExclusions: AnyFieldMarkExclusions + get() = access.anyFieldMarkExclusions + override val isEmpty: Boolean get() = access.isEmpty() override fun concat(other: InitialFactAp.Delta): InitialFactAp.Delta { @@ -67,33 +83,55 @@ data class AccessGraphInitialFactAp( if (base != other.base) return emptyList() if (other.access.isEmpty()) { - val filteredDelta = this.access.filter(other.exclusions) ?: return emptyList() + val filteredDelta = this.access + .filter(other.exclusions) + ?.enforceAnyFieldMarkExclusions(other.anyFieldMarkExclusions, keepInitialLevel = true) + ?: return emptyList() val emptyFact = AccessGraphInitialFactAp(base, access.manager.emptyGraph(), exclusions) - return listOf(emptyFact to Delta(filteredDelta)) + return listOf( + emptyFact to Delta( + filteredDelta.withAnyFieldMarkExclusions(other.anyFieldMarkExclusions) + ) + ) } return access.splitDelta(other.access).mapNotNull { (matchedAccess, delta) -> - val filteredDelta = delta.filter(other.exclusions) ?: return@mapNotNull null + val filteredDelta = delta + .filter(other.exclusions) + ?.enforceAnyFieldMarkExclusions( + other.anyFieldMarkExclusions, + keepInitialLevel = matchedAccess.isEmpty(), + ) + ?: return@mapNotNull null val matchedFact = AccessGraphInitialFactAp(base, matchedAccess, exclusions) - matchedFact to Delta(filteredDelta) + matchedFact to Delta( + filteredDelta.withAnyFieldMarkExclusions(other.anyFieldMarkExclusions) + ) } } override fun concat(delta: InitialFactAp.Delta): InitialFactAp { - if (delta.isEmpty) return this delta as Delta + if (delta.isEmpty) return this - val concatenatedGraph = access.concat(delta.access) - return AccessGraphInitialFactAp(base, concatenatedGraph, exclusions) + val filteredDelta = delta.access.enforceAnyFieldMarkExclusions( + delta.anyFieldMarkExclusions, + keepInitialLevel = access.isEmpty(), + ) ?: return this + return AccessGraphInitialFactAp( + base, + access.concat(filteredDelta).withoutAnyFieldMarkExclusions(), + exclusions, + ) } override fun contains(factAp: InitialFactAp): Boolean { factAp as AccessGraphInitialFactAp if (base != factAp.base) return false - return access.containsAll(factAp.access) + return access.containsAllAccessPaths(factAp.access) } override fun compatibilityFilter(typeChecker: FactTypeChecker): FactTypeChecker.FactCompatibilityFilter = diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataFinalApAccess.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataFinalApAccess.kt index 30e1165d0..5f86c61b7 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataFinalApAccess.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataFinalApAccess.kt @@ -6,6 +6,17 @@ import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.common.FinalApAccess interface AutomataFinalApAccess : FinalApAccess { - override fun getFinalAccess(factAp: FinalFactAp): AccessGraph = (factAp as AccessGraphFinalFactAp).access - override fun createFinal(base: AccessPathBase, ap: AccessGraph, ex: ExclusionSet): FinalFactAp = AccessGraphFinalFactAp(base, ap, ex) + override fun getFinalAccess(factAp: FinalFactAp): AccessGraph = + (factAp as AccessGraphFinalFactAp).access + + override fun createFinal( + base: AccessPathBase, + ap: AccessGraph, + ex: ExclusionSet, + ): FinalFactAp = + AccessGraphFinalFactAp( + base, + ap.forExclusions(ex), + ex, + ) } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataInitialFactAbstraction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataInitialFactAbstraction.kt index 86ab75cfd..4bd4722ed 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataInitialFactAbstraction.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataInitialFactAbstraction.kt @@ -45,7 +45,8 @@ class AutomataInitialFactAbstraction(initialStatement: CommonInst) : InitialFact typeChecker: FactTypeChecker ): List> { val basedFacts = addedFacts.getOrCreate(fact.base) - return basedFacts.addAndAbstract(fact.access, typeChecker).map { + val access = fact.access.withoutAnyFieldMarkExclusions() + return basedFacts.addAndAbstract(access, typeChecker).map { Pair( AccessGraphInitialFactAp(fact.base, it, ExclusionSet.Empty), AccessGraphFinalFactAp(fact.base, it, ExclusionSet.Empty) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodAutomataAccessPathSubscription.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodAutomataAccessPathSubscription.kt index b1a5e952f..89c4ef6e2 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodAutomataAccessPathSubscription.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodAutomataAccessPathSubscription.kt @@ -102,7 +102,7 @@ class MethodAutomataAccessPathSubscription : CommonAPSub val (initialAp, final) = edges[edgeIdx] - if (!final.containsAll(summaryInitialFactAp)) { + if (!final.containsAllAccessPaths(summaryInitialFactAp)) { return@forEach } @@ -142,7 +142,7 @@ class MethodAutomataAccessPathSubscription : CommonAPSub, summaryInitialFact: AccessGraph) { for (graph in graphList) { - if (graph.containsAll(summaryInitialFact)) { + if (graph.containsAllAccessPaths(summaryInitialFact)) { dst.add(graph) } } 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..4ec2b39d0 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 @@ -155,7 +155,7 @@ class MethodEdgesInitialToFinalAutomataApSet( collectToListWithPostProcess( collection, { collectTo(it, statement) }, - { AccessGraphFinalFactAp(base, it, exclusion) } + { AccessGraphFinalFactAp(base, it.forExclusions(exclusion), exclusion) } ) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/SideEffectRequirementAutomataApStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/SideEffectRequirementAutomataApStorage.kt index d5b290c0d..85ec5ed61 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/SideEffectRequirementAutomataApStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/SideEffectRequirementAutomataApStorage.kt @@ -158,7 +158,7 @@ class SideEffectRequirementAutomataApStorage : SideEffectRequirementApStorage { relevantGraphs.forEach { graphIdx -> val graph = requirementGraphs[graphIdx] - if (!factAccess.containsAll(graph)) { + if (!factAccess.containsAllAccessPaths(graph)) { return@forEach } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt index 29eef587f..fcf79bcc3 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt @@ -14,8 +14,12 @@ 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.AnyFieldMarkExclusions import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.clean +import org.opentaint.dataflow.ap.ifds.access.forExclusions +import org.opentaint.dataflow.taint.Cleaner import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext import org.opentaint.dataflow.ap.ifds.serialization.readEnum import org.opentaint.dataflow.ap.ifds.serialization.writeEnum @@ -27,12 +31,21 @@ typealias Cycle = List class AccessCactus( override val base: AccessPathBase, val access: AccessNode, - override val exclusions: ExclusionSet + override val exclusions: ExclusionSet, ): FinalFactAp { + val anyFieldMarkExclusions: AnyFieldMarkExclusions + get() = access.anyFieldMarkExclusions + + private val accessPaths: AccessNode + get() = access.withoutAnyFieldMarkExclusions() + init { assert({ access.isWellFormed() }) { "Ill-formed AccessTree" } + check(exclusions !is ExclusionSet.Universe || anyFieldMarkExclusions.isEmpty) { + "Universe facts cannot carry AnyField mark exclusions" + } } override fun rebase(newBase: AccessPathBase): FinalFactAp = @@ -41,8 +54,17 @@ class AccessCactus( override fun exclude(accessor: Accessor): FinalFactAp = AccessCactus(base, access, exclusions.add(accessor)) + // Cleaner state belongs to the root access value, not to its recursive children. + override fun abstractPart(): FinalFactAp = + AccessCactus( + base, + AccessNode.create(isAbstract = true) + .withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusions, + ) + override fun replaceExclusions(exclusions: ExclusionSet): FinalFactAp = - AccessCactus(base, access, exclusions) + AccessCactus(base, access.forExclusions(exclusions), exclusions) override fun getAllAccessors(): Set { val result = hashSetOf() @@ -55,25 +77,81 @@ class AccessCactus( override fun isAbstract(): Boolean = access.isAbstract override fun readAccessor(accessor: Accessor): FinalFactAp? = - access.getChild(accessor)?.let { AccessCactus(base, it, exclusions) } + accessPaths.getChild(accessor)?.let { + AccessCactus( + base, + it.withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusions, + ) + } - override fun prependAccessor(accessor: Accessor): FinalFactAp = - AccessCactus(base, access.addParent(accessor), exclusions) + override fun prependAccessor(accessor: Accessor): FinalFactAp { + val prepended = accessPaths + .addParent(accessor) + .withAnyFieldMarkExclusions(anyFieldMarkExclusions) + return AccessCactus(base, prepended, exclusions) + } override fun clearAccessor(accessor: Accessor): FinalFactAp? { - val newAccess = access.clearChild(accessor).takeIf { !it.isEmpty } ?: return null - return AccessCactus(base, newAccess, exclusions) + val newAccess = accessPaths.clearChild(accessor).takeIf { !it.isEmpty } ?: return null + return AccessCactus( + base, + newAccess.withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusions, + ) } override fun removeAbstraction(): FinalFactAp? = - access.removeAbstraction().takeIf { !it.isEmpty }?.let { AccessCactus(base, it, exclusions) } + accessPaths.removeAbstraction().takeIf { !it.isEmpty }?.let { + AccessCactus( + base, + it.withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusions, + ) + } override fun abstractOnly(): FinalFactAp = AccessCactus(base, AccessNode.create(isAbstract = true), exclusions) override fun filterFact(filter: FactTypeChecker.FactApFilter): FinalFactAp? { - val filteredAccess = access.filterAccessNode(filter) ?: return null - return AccessCactus(base, filteredAccess, exclusions) + val filteredAccess = accessPaths.filterAccessNode(filter) ?: return null + return AccessCactus( + base, + filteredAccess.withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusions, + ) + } + + override fun clean(cleaner: Cleaner): FinalFactAp.CleanResult = + clean(cleaner, ::cleanAnyField) + + private fun cleanAnyField(mark: TaintMarkAccessor): FinalFactAp.CleanResult { + val belowBaseFilter = object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + if (accessor == mark) { + FactTypeChecker.FilterResult.Reject + } else { + FactTypeChecker.FilterResult.FilterNext(this) + } + } + val atBaseFilter = object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + FactTypeChecker.FilterResult.FilterNext(belowBaseFilter) + } + val cleaned = accessPaths.filterAccessNode(atBaseFilter) + ?: return FinalFactAp.CleanResult(emptyList(), removedAlternative = true) + val cleanedAnyFieldMarkExclusions = + anyFieldMarkExclusions.add(CactusMarkInterner.index(mark)).forExclusions(exclusions) + return FinalFactAp.CleanResult( + survivingFacts = listOf( + AccessCactus( + base, + cleaned.withAnyFieldMarkExclusions(cleanedAnyFieldMarkExclusions), + exclusions, + ) + ), + removedAlternative = false, + ) } // todo: rewrite stub implementation @@ -92,9 +170,13 @@ class AccessCactus( override fun getStartAccessors(): Set = access.allEdges.mapTo(hashSetOf()) { it.accessor } - private sealed interface Delta : FinalFactAp.Delta + sealed interface Delta : FinalFactAp.Delta { + val anyFieldMarkExclusions: AnyFieldMarkExclusions + } - data object EmptyDelta : Delta { + data class EmptyDelta( + override val anyFieldMarkExclusions: AnyFieldMarkExclusions, + ) : Delta { override val isEmpty: Boolean get() = true override fun startsWithAccessor(accessor: Accessor): Boolean = false override fun getStartAccessors(): Set = emptySet() @@ -103,7 +185,10 @@ class AccessCactus( override fun isAbstract(): Boolean = true } - data class NodeDelta(val node: AccessNode) : Delta { + data class NodeDelta( + val node: AccessNode, + override val anyFieldMarkExclusions: AnyFieldMarkExclusions, + ) : Delta { override val isEmpty: Boolean get() = false override fun startsWithAccessor(accessor: Accessor): Boolean = node.contains(accessor) override fun getStartAccessors(): Set = node.allEdges.mapTo(hashSetOf()) { it.accessor } @@ -113,7 +198,7 @@ class AccessCactus( return s } override fun readAccessor(accessor: Accessor): FinalFactAp.Delta? = - node.getChild(accessor)?.let { NodeDelta(it) } + node.getChild(accessor)?.let { NodeDelta(it, anyFieldMarkExclusions) } override fun isAbstract(): Boolean = node.isAbstract } @@ -124,7 +209,7 @@ class AccessCactus( val apRefinements = mutableListOf() var emptyDeltaNeeded = false - CactusUtils.matchAccessPathWithCactus(access, other.access, onFinalMatch = { _ -> + CactusUtils.matchAccessPathWithCactus(accessPaths, other.access, onFinalMatch = { _ -> emptyDeltaNeeded = true }) { treeNode -> val filteredNode = when (val exclusion = other.exclusions) { @@ -150,20 +235,49 @@ class AccessCactus( return buildList { if (emptyDeltaNeeded) { - add(EmptyDelta) + add(EmptyDelta(anyFieldMarkExclusions)) } if (apRefinements.isNotEmpty()) { - addAll(apRefinements.map(AccessCactus::NodeDelta)) + addAll(apRefinements.map { NodeDelta(it, anyFieldMarkExclusions) }) } } } override fun concat(typeChecker: FactTypeChecker, delta: FinalFactAp.Delta): FinalFactAp? { when (val d = delta as Delta) { - EmptyDelta -> return this + is EmptyDelta -> { + val composedAnyFieldMarkExclusions = + (anyFieldMarkExclusions then d.anyFieldMarkExclusions) + .forExclusions(exclusions) + return AccessCactus( + base, + access.withAnyFieldMarkExclusions(composedAnyFieldMarkExclusions), + exclusions, + ) + } is NodeDelta -> { - val concatenatedAccess = access.concatToLeafAbstractNodes(typeChecker, d.node) ?: return null - return AccessCactus(base, concatenatedAccess, exclusions) + val filteredDelta = d.node.enforceAnyFieldMarkExclusions(d.anyFieldMarkExclusions) + ?: return AccessCactus( + base, + access.withAnyFieldMarkExclusions( + (anyFieldMarkExclusions then d.anyFieldMarkExclusions) + .forExclusions(exclusions) + ), + exclusions, + ) + val concatenatedAccess = accessPaths + .concatToLeafAbstractNodes(typeChecker, filteredDelta) + ?: return null + val composedAnyFieldMarkExclusions = + (anyFieldMarkExclusions then d.anyFieldMarkExclusions) + .forExclusions(exclusions) + return AccessCactus( + base, + concatenatedAccess.withAnyFieldMarkExclusions( + composedAnyFieldMarkExclusions + ), + exclusions, + ) } } } @@ -190,7 +304,6 @@ class AccessCactus( if (base != other.base) return false if (access != other.access) return false if (exclusions != other.exclusions) return false - return true } @@ -204,8 +317,22 @@ class AccessCactus( class AccessNode private constructor( val isAbstract: Boolean, val isFinal: Boolean, - val allEdges: Array + val allEdges: Array, + val anyFieldMarkExclusions: AnyFieldMarkExclusions = AnyFieldMarkExclusions.Empty, ) { + fun withAnyFieldMarkExclusions(exclusions: AnyFieldMarkExclusions): AccessNode = + if (exclusions === anyFieldMarkExclusions) { + this + } else { + AccessNode(isAbstract, isFinal, allEdges, exclusions) + } + + fun withoutAnyFieldMarkExclusions(): AccessNode = + withAnyFieldMarkExclusions(AnyFieldMarkExclusions.Empty) + + fun forExclusions(exclusions: ExclusionSet): AccessNode = + withAnyFieldMarkExclusions(anyFieldMarkExclusions.forExclusions(exclusions)) + sealed interface Edge { val accessor: Accessor @@ -321,6 +448,7 @@ class AccessCactus( val fieldHash = allEdges.sumOf { it.hashCode() } hash += fieldHash shl 5 } + hash = 31 * hash + anyFieldMarkExclusions.hashCode() this.hash = hash } @@ -353,6 +481,7 @@ class AccessCactus( if (hash != other.hash) return false if (isAbstract != other.isAbstract || isFinal != other.isFinal) return false + if (anyFieldMarkExclusions != other.anyFieldMarkExclusions) return false return allEdges.contentEquals(other.allEdges) } @@ -733,11 +862,18 @@ class AccessCactus( fun mergeAdd(other: AccessNode, rootAccessors: List = emptyList()): AccessNode { if (this == other) return this - return mergeNodes( + val merged = mergeNodes( other, rootAccessors, onOtherEdge = { _ -> } ) { _, newRootAccessors, thisNode, otherNode -> thisNode.mergeAdd(otherNode, newRootAccessors) - }.also { if (it == this) return this else if (it == other) return other } + } + val result = merged.withAnyFieldMarkExclusions( + anyFieldMarkExclusions join other.anyFieldMarkExclusions + ) + return result.also { + if (it == this) return this + if (it == other) return other + } } fun mergeAddDelta(other: AccessNode): Pair { @@ -768,6 +904,33 @@ class AccessCactus( } } + fun enforceAnyFieldMarkExclusions( + exclusions: AnyFieldMarkExclusions, + keepInitialLevel: Boolean = true, + ): AccessNode? { + if (exclusions.isEmpty) return this + val effective = if (keepInitialLevel) exclusions else exclusions.collapseToDepth1() + + fun exclusionFilter( + current: AnyFieldMarkExclusions, + ): FactTypeChecker.FactApFilter = object : FactTypeChecker.FactApFilter { + override fun check(accessor: Accessor): FactTypeChecker.FilterResult = + if ( + accessor is TaintMarkAccessor && + current.marksFromDepth1.binarySearch(CactusMarkInterner.index(accessor)) >= 0 + ) { + FactTypeChecker.FilterResult.Reject + } else { + val below = current.collapseToDepth1() + FactTypeChecker.FilterResult.FilterNext( + if (below == current) this else exclusionFilter(below) + ) + } + } + + return filterAccessNode(exclusionFilter(effective)) + } + fun concatToLeafAbstractNodes(typeChecker: FactTypeChecker?, other: AccessNode): AccessNode? = concatToLeafAbstractNodes( typeChecker, other, mutableListOf() @@ -838,13 +1001,15 @@ class AccessCactus( val base = createAbstractNodeFromAp(accessPath) if (finalAccessorReached) { - return base + return base.withAnyFieldMarkExclusions(anyFieldMarkExclusions) } if (matchedNodes.isEmpty()) { return null } - return base.concatToLeafAbstractNodes(null, matchedNodes.reduce(AccessNode::mergeAdd)) + return base + .concatToLeafAbstractNodes(null, matchedNodes.reduce(AccessNode::mergeAdd)) + ?.withAnyFieldMarkExclusions(anyFieldMarkExclusions) } private fun squashAndMerge( @@ -1358,4 +1523,4 @@ class AccessCactus( } } } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt index f0ac9010b..dc97e6f22 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt @@ -10,7 +10,7 @@ import org.opentaint.dataflow.ap.ifds.access.InitialFactAp class AccessPathWithCycles( override val base: AccessPathBase, val access: AccessNode?, - override val exclusions: ExclusionSet + override val exclusions: ExclusionSet, ): InitialFactAp { override fun rebase(newBase: AccessPathBase): InitialFactAp = AccessPathWithCycles(newBase, access, exclusions) @@ -68,6 +68,7 @@ class AccessPathWithCycles( // todo: rewrite stub implementation override fun concat(delta: InitialFactAp.Delta): InitialFactAp { + delta as AccessCactus.Delta return this } @@ -101,7 +102,6 @@ class AccessPathWithCycles( if (base != other.base) return false if (access != other.access) return false if (exclusions != other.exclusions) return false - return true } @@ -229,4 +229,4 @@ class AccessPathWithCycles( } } } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusFinalApAccess.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusFinalApAccess.kt index 8defcf2b7..3a8c25963 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusFinalApAccess.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusFinalApAccess.kt @@ -9,6 +9,14 @@ interface CactusFinalApAccess: FinalApAccess { override fun getFinalAccess(factAp: FinalFactAp): AccessCactus.AccessNode = (factAp as AccessCactus).access - override fun createFinal(base: AccessPathBase, ap: AccessCactus.AccessNode, ex: ExclusionSet): FinalFactAp = - AccessCactus(base, ap, ex) + override fun createFinal( + base: AccessPathBase, + ap: AccessCactus.AccessNode, + exclusion: ExclusionSet, + ): FinalFactAp = + AccessCactus( + base, + ap.forExclusions(exclusion), + exclusion, + ) } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusInitialFactAbstraction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusInitialFactAbstraction.kt index b27de0e04..b8992eda1 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusInitialFactAbstraction.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusInitialFactAbstraction.kt @@ -22,7 +22,8 @@ class CactusInitialFactAbstraction: InitialFactAbstraction { // note: we can ignore fact exclusions here val facts = initialFacts.getOrPut(factAp.base) - val addedFact = facts.addInitialFact(factAp.access) ?: return emptyList() + val access = factAp.access.withoutAnyFieldMarkExclusions() + val addedFact = facts.addInitialFact(access) ?: return emptyList() val abstractFacts = mutableListOf>() addAbstractInitialFact(facts, factAp.base, addedFact, abstractFacts) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusMarkInterner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusMarkInterner.kt new file mode 100644 index 000000000..4f4ec1f1e --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusMarkInterner.kt @@ -0,0 +1,17 @@ +package org.opentaint.dataflow.ap.ifds.access.cactus + +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner + +/** Converts Cactus mark objects to the indices used by [AnyFieldMarkExclusions]. */ +internal object CactusMarkInterner { + private val accessors = AccessorInterner() + + fun index(mark: TaintMarkAccessor): AccessorIdx = accessors.index(mark) + + fun mark(index: AccessorIdx): TaintMarkAccessor = + accessors.accessor(index) as? TaintMarkAccessor + ?: error("Cactus AnyField exclusion is not a taint mark: $index") +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusSerializer.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusSerializer.kt index fd90fd7be..d23eac9ad 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusSerializer.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusSerializer.kt @@ -1,8 +1,10 @@ package org.opentaint.dataflow.ap.ifds.access.cactus +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ap.ifds.serialization.AccessPathBaseSerializer +import org.opentaint.dataflow.ap.ifds.serialization.AnyFieldMarkExclusionsSerializer import org.opentaint.dataflow.ap.ifds.serialization.ApSerializer import org.opentaint.dataflow.ap.ifds.serialization.ExclusionSetSerializer import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext @@ -11,16 +13,24 @@ import java.io.DataOutputStream internal class CactusSerializer(private val context : SummarySerializationContext) : ApSerializer { private val accessNodeSerializer = AccessCactus.AccessNode.Serializer(context) - private val exclusionSetSerializer = ExclusionSetSerializer(context) + private val exclusionSerializer = ExclusionSetSerializer(context) + private val anyFieldMarkExclusionsSerializer = AnyFieldMarkExclusionsSerializer( + context, + { CactusMarkInterner.index(it as TaintMarkAccessor) }, + CactusMarkInterner::mark, + ) override fun DataOutputStream.writeFinalAp(ap: FinalFactAp) { (ap as AccessCactus) with (AccessPathBaseSerializer) { writeAccessPathBase(ap.base) } - with (exclusionSetSerializer) { + with (exclusionSerializer) { writeExclusionSet(ap.exclusions) } + with(anyFieldMarkExclusionsSerializer) { + writeAnyFieldMarkExclusions(ap.anyFieldMarkExclusions) + } with (accessNodeSerializer) { writeAccessNode(ap.access) } @@ -31,7 +41,7 @@ internal class CactusSerializer(private val context : SummarySerializationContex with (AccessPathBaseSerializer) { writeAccessPathBase(ap.base) } - with (exclusionSetSerializer) { + with (exclusionSerializer) { writeExclusionSet(ap.exclusions) } val nodes = ap.access?.toList() ?: emptyList() @@ -53,20 +63,27 @@ internal class CactusSerializer(private val context : SummarySerializationContex val base = with (AccessPathBaseSerializer) { readAccessPathBase() } - val exclusions = with (exclusionSetSerializer) { + val exclusion = with (exclusionSerializer) { readExclusionSet() } + val anyFieldMarkExclusions = with(anyFieldMarkExclusionsSerializer) { + readAnyFieldMarkExclusions() + } val access = with (accessNodeSerializer) { readAccessNode() } - return AccessCactus(base, access, exclusions) + return AccessCactus( + base, + access.withAnyFieldMarkExclusions(anyFieldMarkExclusions), + exclusion, + ) } override fun DataInputStream.readInitialAp(): InitialFactAp { val base = with(AccessPathBaseSerializer) { readAccessPathBase() } - val exclusions = with (exclusionSetSerializer) { + val exclusion = with (exclusionSerializer) { readExclusionSet() } val nodesSize = readInt() @@ -84,6 +101,6 @@ internal class CactusSerializer(private val context : SummarySerializationContex } val access = nodeBuilder.build() - return AccessPathWithCycles(base, access, exclusions) + return AccessPathWithCycles(base, access, exclusion) } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt index dafd8f687..64d2bce21 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt @@ -8,11 +8,16 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet 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.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions.Companion.addMarkFromDepth1 +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions.Companion.addMarkFromDepth2 import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.FactTypeChecker import org.opentaint.dataflow.ap.ifds.FinalAccessor import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.clean import org.opentaint.dataflow.ap.ifds.access.tree.AccessPath.AccessNode.Companion.ReversedApNode import org.opentaint.dataflow.ap.ifds.access.tree.AccessPath.AccessNode.Companion.foldRight import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx @@ -26,6 +31,8 @@ import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isS import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext +import org.opentaint.dataflow.ap.ifds.serialization.AnyFieldMarkExclusionsSerializer +import org.opentaint.dataflow.taint.Cleaner import org.opentaint.dataflow.util.Cancellation import org.opentaint.dataflow.util.forEachInt import org.opentaint.dataflow.util.forEachIntEntry @@ -49,6 +56,9 @@ class AccessTree( override fun exclude(accessor: Accessor): FinalFactAp = AccessTree(apManager, base, access, exclusions.add(accessor)) + override fun abstractPart(): FinalFactAp = + AccessTree(apManager, base, access.abstractOnly(), exclusions) + override fun replaceExclusions(exclusions: ExclusionSet): FinalFactAp = AccessTree(apManager, base, access, exclusions) @@ -91,6 +101,18 @@ class AccessTree( override fun abstractOnly(): FinalFactAp = AccessTree(apManager, base, apManager.abstractNode, exclusions) + override fun clean(cleaner: Cleaner): FinalFactAp.CleanResult = + clean(cleaner, ::cleanAnyField) + + private fun cleanAnyField(mark: TaintMarkAccessor): FinalFactAp.CleanResult { + val markIdx = with(apManager) { mark.idx } + val cleaned = access.cleanAnyFieldAtBase(markIdx, IdentityHashMap()) + ?: return FinalFactAp.CleanResult(emptyList(), removedAlternative = true) + + val fact = if (cleaned === access) this else AccessTree(apManager, base, cleaned, exclusions) + return FinalFactAp.CleanResult(listOf(fact), removedAlternative = false) + } + override fun filterFact(filter: FactTypeChecker.FactApFilter): FinalFactAp? { val filteredAccess = access.filterAccessNode(filter) ?: return null return AccessTree(apManager, base, filteredAccess, exclusions) @@ -120,7 +142,14 @@ class AccessTree( private sealed interface AccessTreeDelta : FinalFactAp.Delta - data object EmptyAccessTreeDelta : AccessTreeDelta { + /** + * The abstract remainder of a caller fact matched against a summary's initial AP. It carries + * the caller's excluded marks from the match point: the summary's exit abstraction continues + * the same object, so the claim must ride the summary application onto it. + */ + data class EmptyAccessTreeDelta( + val anyFieldMarkExclusions: AnyFieldMarkExclusions?, + ) : AccessTreeDelta { override val isEmpty: Boolean get() = true override fun startsWithAccessor(accessor: Accessor): Boolean = false override fun getStartAccessors(): Set = emptySet() @@ -167,13 +196,17 @@ class AccessTree( access?.toList()?.forEachInt { accessor -> if (accessor == FINAL_ACCESSOR_IDX) { if (!node.isFinal) return emptyList() - return listOf(EmptyAccessTreeDelta) + return listOf(EmptyAccessTreeDelta(anyFieldMarkExclusions = null)) } node = node.getChild(accessor) ?: return emptyList() } - val filteredNode = when (val exclusion = other.exclusions) { + // Tree facts carry a starred sanitizer's claim on their abstract nodes (see + // AnyFieldMarkExclusions), not in the exclusion set, so there is no deep sweep here: + // enforcement happens where content attaches, in concatToLeafAbstractNodes. + val exclusion = other.exclusions + val filteredNode = when (exclusion) { ExclusionSet.Empty -> node is ExclusionSet.Concrete -> node.filter(exclusion) ExclusionSet.Universe -> error("Unexpected universe exclusion in initial fact") @@ -188,12 +221,17 @@ class AccessTree( .takeIf { !it.isEmpty } ?.let { NodeAccessTreeDelta(apManager, it) } - return listOfNotNull(nonAbstractDelta, EmptyAccessTreeDelta) + return listOfNotNull(nonAbstractDelta, EmptyAccessTreeDelta(filteredNode.anyFieldMarkExclusions)) } override fun concat(typeChecker: FactTypeChecker, delta: FinalFactAp.Delta): FinalFactAp? { when (val d = delta as AccessTreeDelta) { - EmptyAccessTreeDelta -> return this + is EmptyAccessTreeDelta -> { + val anyFieldMarkExclusions = d.anyFieldMarkExclusions ?: return this + val annotated = access.annotateAbstractNodes(anyFieldMarkExclusions, IdentityHashMap()) + if (annotated === access) return this + return AccessTree(apManager, base, annotated, exclusions) + } is NodeAccessTreeDelta -> { val concatenatedAccess = access.concatToLeafAbstractNodes(typeChecker, d.node) ?: return null @@ -240,6 +278,11 @@ class AccessTree( @JvmField val interned: Boolean, @JvmField val isAbstract: Boolean, @JvmField val isFinal: Boolean, + /** + * Marks excluded from future AnyField growth; null when the node is not abstract or has no + * such exclusions (the overwhelmingly common case, so plain nodes pay nothing). + */ + @JvmField val anyFieldMarkExclusions: AnyFieldMarkExclusions?, @JvmField val accessors: IntArray?, @JvmField val accessorNodes: Array?, ) { @@ -248,12 +291,19 @@ class AccessTree( @JvmField val maxDepth: Int @JvmField val containsStatic: Boolean + init { + check(anyFieldMarkExclusions == null || isAbstract) { + "AnyFieldMarkExclusions on a non-abstract node" + } + } + init { var hash = 0L var depth = 0 var containsStatic = false if (isAbstract) hash += 1 + if (anyFieldMarkExclusions != null) hash += anyFieldMarkExclusions.hashCode().toLong() shl 3 if (isFinal) { depth = 1 @@ -298,6 +348,7 @@ class AccessTree( if (hash != other.hash) return false if (isAbstract != other.isAbstract || isFinal != other.isFinal) return false + if (anyFieldMarkExclusions != other.anyFieldMarkExclusions) return false if (!accessors.contentEquals(other.accessors)) return false return accessorNodes.contentEquals(other.accessorNodes) @@ -320,7 +371,8 @@ class AccessTree( if (isFinal) { appendLine(FinalAccessor.toSuffix()) } else { - appendLine("/*$suffix") + val annotation = anyFieldMarkExclusions?.toString().orEmpty() + appendLine("/*$annotation$suffix") } } @@ -471,8 +523,11 @@ class AccessTree( } fun splitOnMatching(otherAccess: AccessPath.AccessNode?): MatchResult { + // An annotated abstraction never matches: the id-edge storage represents the matched + // part as the initial fact's plain abstraction, which would silently drop the + // excluded marks. Such an edge is stored with its real exit tree instead. if (otherAccess == null) { - if (!isAbstract) return MatchResult.NotMatched + if (!isAbstract || anyFieldMarkExclusions != null) return MatchResult.NotMatched val remainder = removeAbstraction().takeIf { !it.isEmpty } return MatchResult.MatchedWithRemainder(remainder) @@ -493,7 +548,7 @@ class AccessTree( ?: return MatchResult.NotMatched } - if (!node.isAbstract) return MatchResult.NotMatched + if (!node.isAbstract || node.anyFieldMarkExclusions != null) return MatchResult.NotMatched val remainder = this.reconstructRemainder(accessorsOnPath, idx = 0) return MatchResult.MatchedWithRemainder(remainder) @@ -528,7 +583,52 @@ class AccessTree( ?: error("Impossible accessor") fun removeAbstraction(): AccessNode = - manager.create(isAbstract = false, isFinal, accessors, accessorNodes) + // The exclusions apply to future AnyField growth and die with the abstraction. + manager.create(isAbstract = false, isFinal, anyFieldMarkExclusions = null, accessors, accessorNodes) + + /** + * The enforcement half of [FinalFactAp.clean]: content being attached below an + * annotated abstract node loses the excluded marks at the depths the annotation claims. + * Returns null when nothing of the attachment survives. + * + * Removing the concrete marks is not the whole job: the attachment can itself contain + * abstract nodes, and there the continuation is NOT yet known — the fact can still grow + * below them after the attach point's own abstraction is consumed. The exclusions + * therefore outlive the attach point on those nodes: the attachment's root sits at the + * attach point itself and inherits them verbatim, while every node strictly below it is + * at least one accessor down, where each excluded mark applies from relative depth 1. + * Without this, a purely abstract delta passes the mark-removal untouched and comes out + * unprotected — the shape of the in-helper clean-then-read false positive. + */ + private fun AccessNode.filterByAnyFieldMarkExclusions(anyFieldMarkExclusions: AnyFieldMarkExclusions?): AccessNode? { + if (anyFieldMarkExclusions == null) return this + + var filtered: AccessNode? = this + if (anyFieldMarkExclusions.marksFromDepth1.isNotEmpty()) { + val marks = IntOpenHashSet(anyFieldMarkExclusions.marksFromDepth1) + filtered = filtered?.removeAccessors(marks, depth = 1, minPruneDepth = 1) + } + if (anyFieldMarkExclusions.marksFromDepth2.isNotEmpty()) { + val marks = IntOpenHashSet(anyFieldMarkExclusions.marksFromDepth2) + filtered = filtered?.removeAccessors(marks, depth = 1, minPruneDepth = 2) + } + if (filtered == null) return null + + val belowClaim = anyFieldMarkExclusions.collapseToDepth1() + val cache = IdentityHashMap() + var annotated = filtered.transformAccessors { _, node -> + node.annotateAbstractNodes(belowClaim, cache) + } + if (annotated.isAbstract) { + val merged = AnyFieldMarkExclusions.then(annotated.anyFieldMarkExclusions, anyFieldMarkExclusions) + if (merged != annotated.anyFieldMarkExclusions) { + annotated = manager.create( + annotated.isAbstract, annotated.isFinal, merged, annotated.accessors, annotated.accessorNodes + ) + } + } + return annotated + } private fun prependAnyAccessor(): AccessNode { val anyNode = getNodeByAccessor(ANY_ACCESSOR_IDX) @@ -583,7 +683,7 @@ class AccessTree( } fun clearChild(accessor: AccessorIdx): AccessNode = when (accessor) { - FINAL_ACCESSOR_IDX -> manager.create(isAbstract, isFinal = false, accessors, accessorNodes) + FINAL_ACCESSOR_IDX -> manager.create(isAbstract, isFinal = false, anyFieldMarkExclusions, accessors, accessorNodes) else -> removeSingleAccessor(accessor) } @@ -603,7 +703,103 @@ class AccessTree( val accessors = transformedAccessors?.first ?: accessors val accessorNodes = transformedAccessors?.second ?: accessorNodes - return manager.create(isAbstract, isFinal, accessors, accessorNodes) + return manager.create(isAbstract, isFinal, anyFieldMarkExclusions, accessors, accessorNodes) + } + + fun removeAccessors(toRemove: IntOpenHashSet, depth: Int, minPruneDepth: Int): AccessNode? { + manager.cancellation.checkpoint() + + return transformAccessorsNonEmpty { accessor, node -> + if (depth >= minPruneDepth && toRemove.contains(accessor)) { + null + } else { + node.removeAccessors(toRemove, depth + 1, minPruneDepth) + } + } + } + + /** + * The structural whole-subtree clean at the fact's base (see [FinalFactAp.clean]): + * direct mark children of the base survive (the base clean action's territory, mirroring + * the old `minPruneDepth = 2`), everything below at least one accessor loses the mark, + * and abstract nodes pick up the residual claim — the base itself from depth 2, deeper + * nodes from depth 1 because everything below them is already under an accessor of the + * base. Returns null when nothing of the node survives. + */ + fun cleanAnyFieldAtBase(markIdx: AccessorIdx, cache: IdentityHashMap): AccessNode? { + // a direct mark child of the base is at depth 1 and stays; everything else is cleaned + val transformed = transformAccessorsNonEmpty { accessor, node -> + if (accessor == markIdx) node else node.cleanMarkBelowBase(markIdx, cache) + } ?: return null // isEmpty implies neither abstract nor final: nothing survived + + return transformed.annotate(markIdx, fromBase = true) + } + + private fun cleanMarkBelowBase(markIdx: AccessorIdx, cache: IdentityHashMap): AccessNode? { + if (cache.containsKey(this)) return cache[this] + + manager.cancellation.checkpoint() + + val transformed = transformAccessorsNonEmpty { accessor, node -> + if (accessor == markIdx) null else node.cleanMarkBelowBase(markIdx, cache) + } + + val result = transformed?.annotate(markIdx, fromBase = false) + + cache[this] = result + return result + } + + /** + * The node reduced to its abstraction: no concrete children, but the abstraction and its + * excluded marks kept. See [FinalFactAp.abstractPart]. + */ + fun abstractOnly(): AccessNode = + manager.create(isAbstract = true, isFinal = false, anyFieldMarkExclusions, accessors = null, accessorNodes = null) + + private fun annotate(markIdx: AccessorIdx, fromBase: Boolean): AccessNode { + if (!isAbstract) return this + + val annotated = if (fromBase) { + anyFieldMarkExclusions.addMarkFromDepth2(markIdx) + } else { + anyFieldMarkExclusions.addMarkFromDepth1(markIdx) + } + if (annotated == anyFieldMarkExclusions) return this + + return manager.create(isAbstract, isFinal, annotated, accessors, accessorNodes) + } + + /** + * Accumulates the caller's excluded marks (see [EmptyAccessTreeDelta]) onto every abstract + * node of a summary's exit fact: each exit abstraction continues the caller's initial + * abstraction. + */ + fun annotateAbstractNodes( + incoming: AnyFieldMarkExclusions, + cache: IdentityHashMap, + ): AccessNode { + cache[this]?.let { return it } + + manager.cancellation.checkpoint() + + val transformed = transformAccessors { _, node -> + node.annotateAbstractNodes(incoming, cache) + } + + val result = if (!transformed.isAbstract) { + transformed + } else { + val merged = AnyFieldMarkExclusions.then(transformed.anyFieldMarkExclusions, incoming) + if (merged == transformed.anyFieldMarkExclusions) { + transformed + } else { + manager.create(transformed.isAbstract, transformed.isFinal, merged, transformed.accessors, transformed.accessorNodes) + } + } + + cache[this] = result + return result } fun collectAccessorsTo(dst: IntOpenHashSet) { @@ -648,7 +844,7 @@ class AccessTree( if (mergedAccessors == null) return this - return manager.create(isAbstract, isFinal, mergedAccessors.first, mergedAccessors.second) + return manager.create(isAbstract, isFinal, anyFieldMarkExclusions, mergedAccessors.first, mergedAccessors.second) } private data class AccessNodeMergePair(val left: AccessNode, val right: AccessNode) { @@ -668,12 +864,24 @@ class AccessTree( a.mergeAddStep(b, results) } + /** + * Joins the excluded marks of two alternative executions meeting at the same node. "Not + * abstract" is the identity: when only one operand can grow, its exclusions are the only + * relevant ones. Two abstract operands intersect their exclusions. + */ + private fun joinAnyFieldMarkExclusions(other: AccessNode): AnyFieldMarkExclusions? = when { + !this.isAbstract -> other.anyFieldMarkExclusions + !other.isAbstract -> this.anyFieldMarkExclusions + else -> AnyFieldMarkExclusions.join(this.anyFieldMarkExclusions, other.anyFieldMarkExclusions) + } + private fun mergeAddStep( other: AccessNode, results: Object2ObjectOpenHashMap ): AccessNode { val isAbstract = this.isAbstract || other.isAbstract val isFinal = this.isFinal || other.isFinal + val anyFieldMarkExclusions = joinAnyFieldMarkExclusions(other) val mergedAccessors = mergeAccessors( other.accessors, other.accessorNodes, onOtherNode = { _, _ -> } @@ -683,6 +891,7 @@ class AccessTree( if ( isAbstract == this.isAbstract && isFinal == this.isFinal + && anyFieldMarkExclusions == this.anyFieldMarkExclusions && mergedAccessors == null ) { return this @@ -691,7 +900,7 @@ class AccessTree( val accessors = mergedAccessors?.first ?: accessors val accessorNodes = mergedAccessors?.second ?: accessorNodes - return manager.create(isAbstract, isFinal, accessors, accessorNodes) + return manager.create(isAbstract, isFinal, anyFieldMarkExclusions, accessors, accessorNodes) } fun mergeAddDelta(other: AccessNode, foldToAny: Boolean = true): Pair = @@ -707,7 +916,17 @@ class AccessTree( val isFinalDelta = !this.isFinal && other.isFinal val isAbstract = this.isAbstract || other.isAbstract - val isAbstractDelta = !this.isAbstract && other.isAbstract + val anyFieldMarkExclusions = joinAnyFieldMarkExclusions(other) + + // The delta contract: a consumer holding `this` must arrive at the merged result by + // Merging the delta into `this` must produce the joined result. Because excluded marks + // intersect, a changed delta carries the joined exclusions rather than `other`'s. + val anyFieldStateChanged = + isAbstract != this.isAbstract || + anyFieldMarkExclusions != this.anyFieldMarkExclusions + val isAbstractDelta = anyFieldStateChanged && isAbstract + val deltaAnyFieldMarkExclusions = + if (isAbstractDelta) anyFieldMarkExclusions else null val deltaAccessors = IntArrayList() val deltaAccessorNodes = arrayListOf() @@ -730,7 +949,7 @@ class AccessTree( } if ( - isAbstract == this.isAbstract + !anyFieldStateChanged && isFinal == this.isFinal && mergedAccessors == null ) { @@ -738,14 +957,14 @@ class AccessTree( } val delta = manager.create( - isAbstractDelta, isFinalDelta, + isAbstractDelta, isFinalDelta, deltaAnyFieldMarkExclusions, deltaAccessors.toIntArray(), deltaAccessorNodes.toTypedArray(), ).takeIf { !it.isEmpty } val accessors = mergedAccessors?.first ?: accessors val accessorNodes = mergedAccessors?.second ?: accessorNodes - return manager.create(isAbstract, isFinal, accessors, accessorNodes) to delta + return manager.create(isAbstract, isFinal, anyFieldMarkExclusions, accessors, accessorNodes) to delta } private inline fun mergeNodeLoop( @@ -1008,6 +1227,7 @@ class AccessTree( interned = true, isAbstract = isAbstract, isFinal = isFinal, + anyFieldMarkExclusions = anyFieldMarkExclusions, accessors = accessors, accessorNodes = accessorNodes ) @@ -1120,6 +1340,7 @@ class AccessTree( val concatNode = if (isAbstract && other != null) { other.filterTypes(typeChecker, path) ?.node?.limitElementAccess(limit = subsequentArrayElementLimit) + ?.filterByAnyFieldMarkExclusions(anyFieldMarkExclusions) } else null val nestedAccessors = mutableListOf>() @@ -1148,7 +1369,9 @@ class AccessTree( } } - val resultNode = manager.create(isAbstract = false, isFinal, accessors = null, accessorNodes = null) + // Concat consumes the abstraction at the attach point: the continuation is now known, + // so the AnyField mark exclusions have done their job. + val resultNode = manager.create(isAbstract = false, isFinal, anyFieldMarkExclusions = null, accessors = null, accessorNodes = null) .bulkMergeAddAccessors(nestedAccessors) val concatenatedNode = concatNode?.let { resultNode.mergeAdd(it) } ?: resultNode @@ -1292,7 +1515,7 @@ class AccessTree( transformer: (AccessorIdx, AccessNode) -> AccessNode? ): AccessNode { val newAccessors = transformAccessors(accessors, accessorNodes, transformer) ?: return this - return manager.create(isAbstract, isFinal, newAccessors.first, newAccessors.second) + return manager.create(isAbstract, isFinal, anyFieldMarkExclusions, newAccessors.first, newAccessors.second) } private fun limitFieldAccess( @@ -1373,13 +1596,17 @@ class AccessTree( private fun removeSingleAccessor(accessor: AccessorIdx): AccessNode { val newAccessors = removeSingleAccessor(accessor, accessors, accessorNodes) ?: return this - return manager.create(isAbstract, isFinal, newAccessors.first, newAccessors.second) + return manager.create(isAbstract, isFinal, anyFieldMarkExclusions, newAccessors.first, newAccessors.second) } internal class Serializer( val manager: TreeApManager, private val context: SummarySerializationContext ) { + private val anyFieldMarkExclusionsSerializer = with(manager) { + AnyFieldMarkExclusionsSerializer(context, { it.idx }, { it.accessor }) + } + fun DataOutputStream.writeAccessNode(node: AccessNode) { var mask = 0 if (node.isFinal) { @@ -1388,8 +1615,17 @@ class AccessTree( if (node.isAbstract) { mask += 2 } + if (node.anyFieldMarkExclusions != null) { + mask += 4 + } write(mask) + node.anyFieldMarkExclusions?.let { + with(anyFieldMarkExclusionsSerializer) { + writeAnyFieldMarkExclusions(it) + } + } + writeInt(node.accessors?.size ?: 0) if (node.accessors != null) { node.accessors.forEach { @@ -1407,9 +1643,18 @@ class AccessTree( val isFinal = mask.and(1) > 0 val isAbstract = mask.and(2) > 0 + val anyFieldMarkExclusions = if (mask.and(4) > 0) { + with(anyFieldMarkExclusionsSerializer) { + readAnyFieldMarkExclusions() + } + } else { + null + } + val accessorsSize = readInt() if (accessorsSize == 0) { - return manager.create(isAbstract, isFinal) + if (anyFieldMarkExclusions == null) return manager.create(isAbstract, isFinal) + return manager.create(isAbstract, isFinal, anyFieldMarkExclusions, accessors = null, accessorNodes = null) } val deserializedAccessors = Array(accessorsSize) { @@ -1436,7 +1681,7 @@ class AccessTree( accessorNodes[dstAccessor] ?: error("Accessor mismatch: $dstAccessor") } - return AccessNode(manager, interned = false, isAbstract, isFinal, accessors, accessNodes) + return AccessNode(manager, interned = false, isAbstract, isFinal, anyFieldMarkExclusions, accessors, accessNodes) } } @@ -1568,6 +1813,7 @@ class AccessTree( manager, interned = true, isAbstract = isAbstract, isFinal = isFinal, + anyFieldMarkExclusions = null, accessors = null, accessorNodes = null ) @@ -1583,6 +1829,7 @@ class AccessTree( node.manager, interned = false, isAbstract = false, isFinal = false, + anyFieldMarkExclusions = null, accessors = intArrayOf(accessor), accessorNodes = arrayOf(node) ) @@ -1591,32 +1838,34 @@ class AccessTree( fun TreeApManager.create( isAbstract: Boolean, isFinal: Boolean, + anyFieldMarkExclusions: AnyFieldMarkExclusions?, accessors: IntArray?, accessorNodes: Array? ): AccessNode = if (isAbstract) { if (isFinal) { - createElementAndField(abstractFinalNode, accessors, accessorNodes) + createElementAndField(abstractFinalNode, anyFieldMarkExclusions, accessors, accessorNodes) } else { - createElementAndField(abstractNode, accessors, accessorNodes) + createElementAndField(abstractNode, anyFieldMarkExclusions, accessors, accessorNodes) } } else { if (isFinal) { - createElementAndField(finalNode, accessors, accessorNodes) + createElementAndField(finalNode, null, accessors, accessorNodes) } else { - createElementAndField(emptyNode, accessors, accessorNodes) + createElementAndField(emptyNode, null, accessors, accessorNodes) } } @JvmStatic private fun createElementAndField( base: AccessNode, + anyFieldMarkExclusions: AnyFieldMarkExclusions?, accessors: IntArray?, accessorNodes: Array?, ): AccessNode { val nonEmptyAccessors = accessors?.takeIf { it.isNotEmpty() } val nonEmptyAccessorNodes = accessorNodes?.takeIf { nonEmptyAccessors != null } - return if (nonEmptyAccessors == null) { + return if (nonEmptyAccessors == null && anyFieldMarkExclusions == null) { base } else { AccessNode( @@ -1624,6 +1873,7 @@ class AccessTree( interned = false, isAbstract = base.isAbstract, isFinal = base.isFinal, + anyFieldMarkExclusions = anyFieldMarkExclusions, accessors = nonEmptyAccessors, accessorNodes = nonEmptyAccessorNodes ) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt index dc5de0dab..77ffa4b82 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt @@ -148,6 +148,12 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { if (!areChildrenChanged && thisFinal == node.isFinal) return node - return manager.create(node.isAbstract, thisFinal, accessorIdx.toIntArray(), accessorNodes.toTypedArray()) + return manager.create( + node.isAbstract, + thisFinal, + node.anyFieldMarkExclusions, + accessorIdx.toIntArray(), + accessorNodes.toTypedArray(), + ) } } 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..cc1e6d320 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 @@ -187,6 +187,7 @@ interface MethodCallFlowFunction { override fun propagateZeroToFactResolutionFailure(currentFactAp: FinalFactAp, startFactBase: AccessPathBase) = buildSet { propagateUnresolvedCallFact( factAp = currentFactAp, + initialFacts = emptySet(), addSideEffectRequirement = { factReader -> check(!factReader.hasRefinement) { "Can't refine Zero fact" } }, @@ -194,6 +195,8 @@ interface MethodCallFlowFunction { check(!factReader.hasRefinement) { "Can't refine Zero fact" } this += CallToReturnZFact(factAp, trace) }, + // no initial facts here, so the resolver is never built/consulted + addSideEffect = { _, _ -> }, ) } @@ -204,6 +207,7 @@ interface MethodCallFlowFunction { ): Set = buildSet { propagateUnresolvedCallFact( factAp = currentFactAp, + initialFacts = setOf(initialFactAp), addSideEffectRequirement = { factReader -> this += SideEffectRequirement(factReader.refineFact(initialFactAp.replaceExclusions(ExclusionSet.Empty))) }, @@ -214,6 +218,7 @@ interface MethodCallFlowFunction { trace ) }, + addSideEffect = { i, k -> this += FactSideEffect(i, k) }, ) } @@ -224,6 +229,7 @@ interface MethodCallFlowFunction { ) = buildSet { propagateUnresolvedCallFact( factAp = currentFactAp, + initialFacts = initialFacts, addSideEffectRequirement = { factReader -> check(!factReader.hasRefinement) { "Can't refine NDF2F edge" } }, @@ -231,6 +237,8 @@ interface MethodCallFlowFunction { check(!factReader.hasRefinement) { "Can't refine NDF2F edge" } this += CallToReturnNonDistributiveFact(initialFacts, factAp, trace) }, + // multiple initial facts here, so the resolver is never built/consulted + addSideEffect = { _, _ -> }, ) } @@ -247,8 +255,10 @@ interface MethodCallFlowFunction { fun propagateUnresolvedCallFact( factAp: FinalFactAp, + initialFacts: Set, addCallToReturn: (FinalFactReader, FinalFactAp, TraceInfo?) -> Unit, addSideEffectRequirement: (FinalFactReader) -> Unit, + addSideEffect: (InitialFactAp, SideEffectKind) -> Unit, ) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallSummaryHandler.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallSummaryHandler.kt index 2e58c6b75..aa60caf7f 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallSummaryHandler.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodCallSummaryHandler.kt @@ -4,8 +4,6 @@ import org.opentaint.dataflow.ap.ifds.Edge import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.FactTypeChecker import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication -import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryApRefinement -import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction.Sequent @@ -97,8 +95,8 @@ interface MethodCallSummaryHandler { fun prepareNDFactToFactSummary(summaryEdge: Edge.NDFactToFact): List = listOf(summaryEdge) - fun InitialFactAp.refine(exclusionSet: ExclusionSet?) = - if (exclusionSet == null) this else replaceExclusions(exclusionSet) + fun InitialFactAp.refine(exclusion: ExclusionSet?) = + if (exclusion == null) this else replaceExclusions(exclusion) fun handleSummary( currentFactAp: FinalFactAp, @@ -108,24 +106,16 @@ interface MethodCallSummaryHandler { handleSummaryEdge: (initialFactRefinement: ExclusionSet?, summaryFactAp: FinalFactAp) -> Sequent ): Set { val mappedSummaryFacts = mapMethodExitToReturnFlowFact(summaryEdge.final) + val initialFactExclusions = summaryEffect.initialFactExclusions + val resultExclusions = initialFactExclusions ?: currentFactAp.exclusions - return when (summaryEffect) { - is SummaryApRefinement -> mappedSummaryFacts.mapNotNullTo(hashSetOf()) { mappedSummaryFact -> - // todo: filter exclusions - val summaryFactAp = mappedSummaryFact - .concat(factTypeChecker, summaryEffect.delta) - ?.replaceExclusions(currentFactAp.exclusions) - ?: return@mapNotNullTo null + return mappedSummaryFacts.mapNotNullTo(hashSetOf()) { mappedSummaryFact -> + val summaryAccess = summaryEffect.accessDelta + ?.let { mappedSummaryFact.concat(factTypeChecker, it) ?: return@mapNotNullTo null } + ?: mappedSummaryFact + val summaryFactAp = summaryAccess.replaceExclusions(resultExclusions) - handleSummaryEdge(null, summaryFactAp) - } - - is SummaryExclusionRefinement -> mappedSummaryFacts.mapTo(hashSetOf()) { mappedSummaryFact -> - // todo: filter exclusions - val summaryFactAp = mappedSummaryFact.replaceExclusions(summaryEffect.exclusion) - - handleSummaryEdge(summaryEffect.exclusion, summaryFactAp) - } + handleSummaryEdge(initialFactExclusions, summaryFactAp) } } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSideEffectSummaryHandler.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSideEffectSummaryHandler.kt index e444138d4..d9d556a9a 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSideEffectSummaryHandler.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/analysis/MethodSideEffectSummaryHandler.kt @@ -1,12 +1,10 @@ package org.opentaint.dataflow.ap.ifds.analysis -import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication -import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryApRefinement -import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement import org.opentaint.dataflow.ap.ifds.SideEffectKind import org.opentaint.dataflow.ap.ifds.SideEffectSummary import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction.Sequent @@ -36,12 +34,9 @@ interface MethodSideEffectSummaryHandler { summaryEffect: SummaryEdgeApplication, kind: SideEffectKind, handleSE: (initialFactRefinement: ExclusionSet, kind: SideEffectKind) -> Sequent - ): Set = when (summaryEffect) { - // Side effect requires more concrete fact - is SummaryApRefinement -> emptySet() - - is SummaryExclusionRefinement -> { - setOf(handleSE(summaryEffect.exclusion, kind)) - } + ): Set { + // A side effect whose match needs a more concrete access path is not applicable yet. + val exclusions = summaryEffect.initialFactExclusions ?: return emptySet() + return setOf(handleSE(exclusions, kind)) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/serialization/AnyFieldMarkExclusionsSerializer.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/serialization/AnyFieldMarkExclusionsSerializer.kt new file mode 100644 index 000000000..70cc0e77f --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/serialization/AnyFieldMarkExclusionsSerializer.kt @@ -0,0 +1,34 @@ +package org.opentaint.dataflow.ap.ifds.serialization + +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions +import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import java.io.DataInputStream +import java.io.DataOutputStream + +class AnyFieldMarkExclusionsSerializer( + private val context: SummarySerializationContext, + private val index: (Accessor) -> AccessorIdx, + private val accessor: (AccessorIdx) -> Accessor, +) { + fun DataOutputStream.writeAnyFieldMarkExclusions(exclusions: AnyFieldMarkExclusions) { + writeMarks(exclusions.marksFromDepth1) + writeMarks(exclusions.marksFromDepth2) + } + + private fun DataOutputStream.writeMarks(marks: IntArray) { + writeInt(marks.size) + marks.forEach { writeLong(context.getIdByAccessor(accessor(it))) } + } + + fun DataInputStream.readAnyFieldMarkExclusions(): AnyFieldMarkExclusions { + val depth1 = readMarks() + val depth2 = readMarks() + return AnyFieldMarkExclusions.create(depth1, depth2) ?: AnyFieldMarkExclusions.Empty + } + + private fun DataInputStream.readMarks(): IntArray = + IntArray(readInt()) { + index(context.getAccessorById(readLong())) + }.also(IntArray::sort) +} 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..a6a4fb5d1 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 @@ -1,11 +1,37 @@ package org.opentaint.dataflow.taint import org.opentaint.dataflow.ap.ifds.Accessor -import org.opentaint.dataflow.ap.ifds.AnyAccessor import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.configuration.CommonTaintAction import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.TaintCleanReach + +/** A cleaner expressed in the same position language as sources and sinks. */ +sealed interface Cleaner { + val position: PositionAccess + + data class AllMarks( + override val position: PositionAccess, + ) : Cleaner + + data class Mark( + override val position: PositionAccess, + val mark: TaintMarkAccessor, + val reach: TaintCleanReach = TaintCleanReach.Exact, + ) : Cleaner +} + +internal val Cleaner.requiresDemandResolution: Boolean + get() = this is Cleaner.AllMarks || !position.hasAnyField() + +internal fun Cleaner.removePrefix(prefix: Accessor): Cleaner { + val remainingPosition = position.removePrefix(prefix) + return when (this) { + is Cleaner.AllMarks -> copy(position = remainingPosition) + is Cleaner.Mark -> copy(position = remainingPosition) + } +} class TaintCleanActionEvaluator { fun removeAllFacts( @@ -15,16 +41,9 @@ class TaintCleanActionEvaluator { action: CommonTaintAction, ): List { val fact = evc.fact ?: return listOf(evc) - - if (!fact.containsPosition(from)) return listOf(evc) - - if (from is PositionAccess.Simple) { - val actionInfo = EvaluatedCleanAction.ActionInfo(rule, action) - return listOf(EvaluatedCleanAction(fact = null, actionInfo, evc)) - } - - val cleanAccessors = from.accessorList() - return cleanAccessors(cleanAccessors, fact, rule, action, evc) + if (from.base() != fact.factAp.base) return listOf(evc) + val cleaned = fact.clean(Cleaner.AllMarks(from)) ?: return listOf(evc) + return clean(cleaned, fact, rule, action, evc) } fun removeFinalFact( @@ -33,81 +52,33 @@ class TaintCleanActionEvaluator { markRestriction: TaintMarkAccessor, rule: CommonTaintConfigurationItem, action: CommonTaintAction, + reach: TaintCleanReach = TaintCleanReach.Exact, ): List { val fact = evc.fact ?: return listOf(evc) - - if (!fact.containsPositionWithTaintMark(from, markRestriction)) return listOf(evc) - - val cleanAccessors = from.accessorList() + markRestriction - return cleanAccessors(cleanAccessors, fact, rule, action, evc) + if (from.base() != fact.factAp.base) return listOf(evc) + val cleaned = fact.clean(Cleaner.Mark(from, markRestriction, reach)) ?: return listOf(evc) + return clean(cleaned, fact, rule, action, evc) } - private fun cleanAccessors( - accessors: List, + private fun clean( + cleaned: FinalFactAp.CleanResult, fact: FinalFactReader, rule: CommonTaintConfigurationItem, action: CommonTaintAction, evc: EvaluatedCleanAction ): List { - val (cleanedFacts, factCleaned) = clearPosition(accessors, fact.factAp) - val result = mutableListOf() - if (factCleaned) { + if (cleaned.removedAlternative) { val actionInfo = EvaluatedCleanAction.ActionInfo(rule, action) result += EvaluatedCleanAction(null, actionInfo, evc) } - return cleanedFacts.mapTo(result) { cleanedFact -> + return cleaned.survivingFacts.mapTo(result) { cleanedFact -> val resultFact = fact.replaceFact(cleanedFact) val actionInfo = EvaluatedCleanAction.ActionInfo(rule, action) EvaluatedCleanAction(resultFact, actionInfo, evc) } } - - private fun clearPosition(accessors: List, fact: FinalFactAp): Pair, Boolean> { - val head = accessors.first() - val tail = accessors.drop(1) - if (tail.isEmpty()) { - if (fact.startsWithAccessor(AnyAccessor)) { - val factAfterAny = fact.readAccessor(AnyAccessor) - ?: error("Impossible") - - val clearedAfterAny = factAfterAny.clearAccessor(head) - val restoredAfterAny = clearedAfterAny?.prependAccessor(AnyAccessor) - - val factWithoutAny = fact.clearAccessor(AnyAccessor) - val cleanedWithoutAny = factWithoutAny?.clearAccessor(head) - - val cleaned = clearedAfterAny != factAfterAny || cleanedWithoutAny != factWithoutAny - - return listOfNotNull(restoredAfterAny, cleanedWithoutAny) to cleaned - } - - if (!fact.startsWithAccessor(head)) { - return listOf(fact) to false - } - - val clearedFact = fact.clearAccessor(head) - val cleaned = clearedFact != fact - - return listOfNotNull(clearedFact) to cleaned - } - - val child = fact.readAccessor(head) - ?: return listOf(fact) to false - - val remaining = listOfNotNull(fact.clearAccessor(head)) - val (cleanChild, childCleaned) = clearPosition(tail, child) - val cleanChildWithAccessor = cleanChild.map { it.prependAccessor(head) } - val fullFact = remaining + cleanChildWithAccessor - - return fullFact to childCleaned - } - - private fun PositionAccess.accessorList(): List = when (this) { - is PositionAccess.Simple -> emptyList() - is PositionAccess.Complex -> base.accessorList() + accessor - } } inline fun List.applyCleanerActions( 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..4be3c1230 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 @@ -49,6 +49,18 @@ class FinalFactReader( matchedNode = { true } ) + fun clean(cleaner: Cleaner): FinalFactAp.CleanResult? { + if (cleaner.requiresDemandResolution) { + val present = when (cleaner) { + is Cleaner.AllMarks -> containsPosition(cleaner.position) + is Cleaner.Mark -> containsPositionWithTaintMark(cleaner.position, cleaner.mark) + } + if (!present) return null + } + + return factAp.clean(cleaner) + } + fun replaceFact(factAp: FinalFactAp) = FinalFactReader(factAp, apManager).also { it.refinement = refinement } fun refineFact(factAp: InitialFactAp): InitialFactAp { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReaderUtils.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReaderUtils.kt index b0b87a4be..471b35eb9 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReaderUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/FactReaderUtils.kt @@ -80,7 +80,7 @@ inline fun readWithAnyAccessorSplit( } val firstMismatch = mismatchedNodes.firstOrNull() - ?: error("Impossible") + ?: return onMismatch(ap, null) return onMismatch(firstMismatch.first, firstMismatch.second) } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/MethodSideEffectHandlerWithAnyAccessorRequestHandling.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/MethodSideEffectHandlerWithAnyAccessorRequestHandling.kt index 9485b9cd6..b8eb96d64 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/MethodSideEffectHandlerWithAnyAccessorRequestHandling.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/MethodSideEffectHandlerWithAnyAccessorRequestHandling.kt @@ -19,16 +19,9 @@ interface MethodSideEffectHandlerWithAnyAccessorRequestHandling : MethodSideEffe kind: SideEffectKind ): Set { if (kind is TaintMarkFieldUnfoldRequest) { - when (summaryEffect) { - is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryApRefinement -> { - if (!summaryEffect.delta.isEmpty) { - handleMarkAfterAnyFieldRequest(summaryEffect.delta, kind) - } - } - - is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement -> { - // taint mark requested -> mark not in initial fact, delta is empty -> mark not in fact - } + val delta = summaryEffect.accessDelta + if (delta != null && !delta.isEmpty) { + handleMarkAfterAnyFieldRequest(delta, kind) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/PositionAccess.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/PositionAccess.kt index 76cf334a3..b4765144b 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/PositionAccess.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/PositionAccess.kt @@ -3,8 +3,6 @@ package org.opentaint.dataflow.taint 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.ElementAccessor -import org.opentaint.dataflow.ap.ifds.FieldAccessor sealed interface PositionAccess { data class Simple(val base: AccessPathBase) : PositionAccess @@ -16,11 +14,6 @@ fun PositionAccess.base(): AccessPathBase = when (this) { is PositionAccess.Simple -> this.base } -private fun PositionAccess.baseIsResult(): Boolean = when (this) { - is PositionAccess.Complex -> base.baseIsResult() - is PositionAccess.Simple -> base is AccessPathBase.Return -} - fun PositionAccess.withPrefix(prefix: Accessor): PositionAccess = when (this) { is PositionAccess.Complex -> PositionAccess.Complex(base.withPrefix(prefix), accessor) is PositionAccess.Simple -> PositionAccess.Complex(this, prefix) @@ -58,3 +51,11 @@ fun PositionAccess.removePrefix(prefix: Accessor): PositionAccess = when (this) is PositionAccess.Simple -> error("Prefix mismatch") } + +fun PositionAccess.accessors(): List = when (this) { + is PositionAccess.Simple -> emptyList() + is PositionAccess.Complex -> base.accessors() + accessor +} + +fun PositionAccess.hasAnyField(): Boolean = + accessors().any { it is AnyAccessor } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/RulePreconditionUtils.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/RulePreconditionUtils.kt index 6348052d1..6d01eca9e 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/RulePreconditionUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/RulePreconditionUtils.kt @@ -24,10 +24,11 @@ fun evaluateSo ruleActions: List, sourcePreconditionEvaluator: TaintSourceActionPreconditionEvaluator, evalAction: TaintSourceActionPreconditionEvaluator.(R, A) -> Maybe>>, + evalProducedFact: TaintSourceActionPreconditionEvaluator.(R, A) -> Maybe>>, ): List { val result = mutableListOf() evaluateSourceRulePrecondition( - ruleWithCond, ruleActions, sourcePreconditionEvaluator, evalAction, + ruleWithCond, ruleActions, sourcePreconditionEvaluator, evalAction, evalProducedFact, mkSource = { r, a -> result += TaintRulePrecondition.Source(r, a) }, mkPass = { r, a, e -> result += TaintRulePrecondition.Pass(r, a, PassRuleCondition.Expr(e)) } ) @@ -39,14 +40,22 @@ fun evaluateSour ruleActions: List, sourcePreconditionEvaluator: TaintSourceActionPreconditionEvaluator, evalAction: TaintSourceActionPreconditionEvaluator.(R, A) -> Maybe>>, + evalProducedFact: TaintSourceActionPreconditionEvaluator.(R, A) -> Maybe>>, mkSource: (R, Set) -> Unit, mkPass: (R, Set, TaintMarkAwareConditionExpr) -> Unit, ) { val rule = ruleWithCond.rule - val assignedMarks = ruleActions.maybeFlatMap { + val directlyAssignedMarks = ruleActions.maybeFlatMap { sourcePreconditionEvaluator.evalAction(rule, it) } + val assignedMarks = if (directlyAssignedMarks.isSome) { + directlyAssignedMarks + } else { + ruleActions.maybeFlatMap { + sourcePreconditionEvaluator.evalProducedFact(rule, it) + } + } if (assignedMarks.isNone) return val sourceActions = assignedMarks.getOrThrow().mapTo(hashSetOf()) { it.second } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Source.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Source.kt index abf53bc0f..3f0b50453 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Source.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/Source.kt @@ -22,7 +22,7 @@ class TaintSourceActionEvaluator( position: PositionAccess, mark: TaintMarkAccessor ): Maybe> { - val fact = apManager.mkAccessPath(position, exclusion, mark) + val fact = apManager.mkAccessPath(position, exclusion, mark) return Maybe.from(listOf(fact)) } } @@ -39,4 +39,16 @@ class TaintSourceActionPreconditionEvaluator( if (!factReader.containsPositionWithTaintMark(position, mark)) return Maybe.none() return Maybe.some(listOf(rule to action)) } + + fun evaluateProducedFact( + rule: CommonTaintConfigurationItem, + action: CommonTaintAssignAction, + position: PositionAccess, + mark: TaintMarkAccessor, + ): Maybe>> { + if (!position.hasAnyField()) return Maybe.none() + val sourceFact = factReader.apManager.mkAccessPath(position, ExclusionSet.Universe, mark) + if (!sourceFact.contains(factReader.fact)) return Maybe.none() + return Maybe.some(listOf(rule to action)) + } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt index f253b8e61..b0986516f 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt @@ -42,28 +42,46 @@ class TaintFactAwareConditionEvaluator( } } + private val anyFieldMarkEvalCache = hashMapOf, MarkEvaluationResult>() + private fun evalContainsMarkOnAnyField(positionAccess: PositionAccess, mark: TaintMarkAccessor): Boolean { val conditionBase = positionAccess.base() val relevantFacts = basedFacts[conditionBase] ?: return false - val requiredPosition = positionAccess.withSuffix(listOf(mark)) - for (reader in relevantFacts) { - val positionWithTaintMark = reader.containsAnyPosition(requiredPosition) ?: continue + val result = anyFieldMarkEvalCache.computeIfAbsent(positionAccess to mark) { + val requiredPosition = positionAccess.withSuffix(listOf(mark)) - val finalPositionWithTaintMark = positionWithTaintMark.withSuffix(listOf(FinalAccessor)) - if (!reader.containsPosition(finalPositionWithTaintMark)) continue + var evaluatedFact: EvaluatedFact? = null + for (reader in relevantFacts) { + val positionWithTaintMark = reader.containsAnyPosition(requiredPosition) ?: continue - val tmPosition = positionWithTaintMark.removeSuffix(listOf(mark)) + val finalPositionWithTaintMark = positionWithTaintMark.withSuffix(listOf(FinalAccessor)) + if (!reader.containsPosition(finalPositionWithTaintMark)) continue - hasEvaluatedContainsMark = true - evaluatedFacts += EvaluatedFact(reader, tmPosition, mark) + val tmPosition = positionWithTaintMark.removeSuffix(listOf(mark)) + evaluatedFact = EvaluatedFact(reader, tmPosition, mark) + break + } - return true + if (evaluatedFact != null) { + evaluatedFact + } else { + // Register the unfold request only on the first computation for this (pos, mark); + // repeated evaluations for the same key are served from the cache without re-resolving. + markAfterAnyAccessorResolver?.resolve(mark) + NoFact + } } - markAfterAnyAccessorResolver?.resolve(mark) + return when (result) { + is NoFact -> false + is EvaluatedFact -> { + hasEvaluatedContainsMark = true + evaluatedFacts += result - return false + true + } + } } private val markEvalCache = hashMapOf, MarkEvaluationResult>() diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt index 10408c49a..33de88226 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt @@ -63,6 +63,7 @@ abstract class TaintUtil(val apManager: ApManager) { createFinalFact: (FinalFactAp, Trace) -> Unit, createEdge: (InitialFactAp, FinalFactAp, Trace) -> Unit, createNDEdge: (Set, FinalFactAp, Trace) -> Unit, + markAfterAnyFieldResolver: FactWithMarkAfterAnyAccessorResolver? = null, ) { if (sourceRules.isEmpty()) return @@ -76,7 +77,7 @@ abstract class TaintUtil(val apManager: ApManager) { apManager, initialFacts, conditionFactReaders, - markAfterAnyFieldResolver = null, // we don't expect such marks in source rules + markAfterAnyFieldResolver = markAfterAnyFieldResolver, assumptionsManager = sourceAssumptionsManager(), applyRule = { rule, evaluatedFacts -> // unconditional sources handled with zero fact diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/AnyFieldMarkExclusionsTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/AnyFieldMarkExclusionsTest.kt new file mode 100644 index 000000000..3b72ec6a6 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/AnyFieldMarkExclusionsTest.kt @@ -0,0 +1,43 @@ +package org.opentaint.dataflow.ap.ifds.access + +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions.Companion.addMarkFromDepth1 +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class AnyFieldMarkExclusionsTest { + @Test + fun `base clean starts at depth two and collapses below an accessor`() { + val atBase = AnyFieldMarkExclusions.Empty.add(7) + assertContentEquals(intArrayOf(7), atBase.marksFromDepth2) + + val belowBase = atBase.collapseToDepth1() + assertContentEquals(intArrayOf(7), belowBase.marksFromDepth1) + assertContentEquals(intArrayOf(), belowBase.marksFromDepth2) + } + + @Test + fun `sequential composition keeps all claims at their strongest depth`() { + val depth1 = AnyFieldMarkExclusions.Empty.addMarkFromDepth1(1) + val depth2 = AnyFieldMarkExclusions.Empty.add(1).add(2) + + val composed = depth1 then depth2 + + assertContentEquals(intArrayOf(1), composed.marksFromDepth1) + assertContentEquals(intArrayOf(2), composed.marksFromDepth2) + } + + @Test + fun `alternative join keeps shared claims at their weakest depth`() { + val stronger = AnyFieldMarkExclusions.Empty + .addMarkFromDepth1(1) + .addMarkFromDepth1(2) + val weaker = AnyFieldMarkExclusions.Empty.add(1).add(3) + + val joined = stronger join weaker + + assertContentEquals(intArrayOf(), joined.marksFromDepth1) + assertContentEquals(intArrayOf(1), joined.marksFromDepth2) + assertEquals(joined, weaker join stronger) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/FactCleanerContractTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/FactCleanerContractTest.kt new file mode 100644 index 000000000..d7df5aa65 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/FactCleanerContractTest.kt @@ -0,0 +1,150 @@ +package org.opentaint.dataflow.ap.ifds.access + +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.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.automata.AutomataApManager +import org.opentaint.dataflow.ap.ifds.access.cactus.CactusApManager +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.configuration.TaintCleanReach +import org.opentaint.dataflow.taint.Cleaner +import org.opentaint.dataflow.taint.FinalFactReader +import org.opentaint.dataflow.taint.PositionAccess +import org.opentaint.dataflow.taint.mkAccessPath +import org.opentaint.dataflow.taint.withSuffix +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class FactCleanerContractTest { + private val base = AccessPathBase.This + private val field = FieldAccessor("Box", "value", "String") + private val nestedField = FieldAccessor("String", "nested", "String") + private val mark = TaintMarkAccessor("tainted") + + private fun cleaner(vararg accessors: Accessor): Cleaner.Mark = + Cleaner.Mark(PositionAccess.Simple(base).withSuffix(accessors.toList()), mark) + + private object UnrollStrategy : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = false + } + + private fun managers(): List = listOf( + TreeApManager(UnrollStrategy, RefManager(), Cancellation()), + AutomataApManager(UnrollStrategy, Cancellation()), + CactusApManager(UnrollStrategy, Cancellation()), + ) + + @Test + fun `every representation implements the same cleaner boundary`() { + for (manager in managers()) { + assertEquals( + 1, + manager.mostAbstractFinalAp(base) + .clean(cleaner(AnyAccessor)) + .survivingFacts.size, + "${manager::class.simpleName} must preserve a cleaned abstract fact", + ) + + var concrete = manager.createFinalAp(base, ExclusionSet.Empty) + for (accessor in listOf(field, mark).asReversed()) { + concrete = concrete.prependAccessor(accessor) + } + + val cleanResult = concrete.clean(cleaner(AnyAccessor)) + assertTrue( + cleanResult.survivingFacts.isEmpty() || + cleanResult.survivingFacts.none { + it.readAccessor(field)?.startsWithAccessor(mark) == true + }, + "${manager::class.simpleName} retained an already-materialized nested mark", + ) + } + } + + @Test + fun `plain and any-field cleaners use the same operation`() { + for (manager in managers()) { + var concrete = manager.createFinalAp(base, ExclusionSet.Empty) + for (accessor in listOf(field, mark).asReversed()) { + concrete = concrete.prependAccessor(accessor) + } + + val plain = concrete.clean(cleaner(field)) + val anyField = concrete.clean(cleaner(AnyAccessor)) + + assertTrue(plain.survivingFacts.isEmpty()) + assertTrue(anyField.survivingFacts.isEmpty()) + } + } + + @Test + fun `any-field keeps its meaning below an exact position`() { + for (manager in managers()) { + var concrete = manager.createFinalAp(base, ExclusionSet.Empty) + for (accessor in listOf(field, nestedField, mark).asReversed()) { + concrete = concrete.prependAccessor(accessor) + } + + val cleaned = concrete.clean(cleaner(field, AnyAccessor)) + + assertTrue( + cleaned.survivingFacts.isEmpty() || + cleaned.survivingFacts.none { + it.readAccessor(field) + ?.readAccessor(nestedField) + ?.startsWithAccessor(mark) == true + }, + "${manager::class.simpleName} changed nested AnyField semantics", + ) + } + } + + @Test + fun `every representation finds a mark behind AnyField`() { + for (manager in managers()) { + val anyPosition = PositionAccess.Simple(base).withSuffix(listOf(AnyAccessor)) + val fact = manager.mkAccessPath(anyPosition, ExclusionSet.Empty, mark) + val requiredMark = PositionAccess.Simple(base).withSuffix(listOf(mark)) + assertNotNull( + FinalFactReader(fact, manager).containsAnyPosition(requiredMark), + "${manager::class.simpleName} did not find $requiredMark in $fact", + ) + } + } + + @Test + fun `mark cleanup explicitly chooses whether AnyField is a target`() { + for (manager in managers()) { + var fact = manager.createFinalAp(base, ExclusionSet.Empty) + for (accessor in listOf(AnyAccessor, mark).asReversed()) { + fact = fact.prependAccessor(accessor) + } + val exactCleaner = cleaner() + val anyFieldCleaner = exactCleaner.copy( + reach = TaintCleanReach.ExactAndAnyField, + ) + + val exactResult = fact.clean(exactCleaner) + val anyFieldResult = fact.clean(anyFieldCleaner) + + assertEquals(listOf(fact), exactResult.survivingFacts) + assertTrue( + anyFieldResult.survivingFacts.isEmpty() || + anyFieldResult.survivingFacts.none { + FinalFactReader(it, manager) + .containsAnyPosition(PositionAccess.Simple(base).withSuffix(listOf(mark))) != null + }, + "${manager::class.simpleName} retained a targeted AnyField mark: " + + "$fact -> ${anyFieldResult.survivingFacts}", + ) + } + } + +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataAccessTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataAccessTest.kt new file mode 100644 index 000000000..224f04fc6 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AutomataAccessTest.kt @@ -0,0 +1,48 @@ +package org.opentaint.dataflow.ap.ifds.access.automata + +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +class AutomataAccessTest { + private val graphGenerator = RandomGraphGenerator() + private val empty = graphGenerator.manager.emptyGraph() + + @Test + fun `merging alternatives treats shape and AnyField mark exclusions as one value`() { + val cleaned = empty.prepend(1) + .withAnyFieldMarkExclusions(AnyFieldMarkExclusions.Empty.add(7)) + val uncleaned = empty.prepend(2) + + val merged = cleaned.merge(uncleaned) + + assertEquals(AnyFieldMarkExclusions.Empty, merged.anyFieldMarkExclusions) + assertEquals(true, merged.containsAll(cleaned)) + assertEquals(true, merged.containsAll(uncleaned)) + } + + @Test + fun `merging a contained value is identity`() { + val access = empty.prepend(1) + + assertSame(access, access.merge(access)) + } + + @Test + fun `the abstract empty graph survives an always-compatible filter`() { + assertSame(empty, empty.filter(FactTypeChecker.AlwaysCompatibleFilter)) + } + + @Test + fun `the abstract empty graph has no trailing accessor for a compatibility filter to reject`() { + val rejectAccessors = object : FactTypeChecker.FactCompatibilityFilter { + override fun check(accessor: Accessor) = + FactTypeChecker.CompatibilityFilterResult.NotCompatible + } + + assertSame(empty, empty.filter(rejectAccessors)) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusAccessTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusAccessTest.kt new file mode 100644 index 000000000..bef55a452 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/CactusAccessTest.kt @@ -0,0 +1,33 @@ +package org.opentaint.dataflow.ap.ifds.access.cactus + +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyFieldMarkExclusions +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class CactusAccessTest { + @Test + fun `cleaner change re-emits the complete access value`() { + val markA = TaintMarkAccessor("a") + val markB = TaintMarkAccessor("b") + val access = AccessCactus.AccessNode.create(isAbstract = true) + val cleanedTwice = access.withAnyFieldMarkExclusions( + AnyFieldMarkExclusions.Empty + .add(CactusMarkInterner.index(markA)) + .add(CactusMarkInterner.index(markB)), + ) + val cleanedOnce = access.withAnyFieldMarkExclusions( + AnyFieldMarkExclusions.Empty.add(CactusMarkInterner.index(markA)), + ) + + val (merged, delta) = cleanedTwice.mergeAddDelta(cleanedOnce) + + assertEquals( + access, + merged.withAnyFieldMarkExclusions(AnyFieldMarkExclusions.Empty), + ) + assertEquals(cleanedOnce.anyFieldMarkExclusions, merged.anyFieldMarkExclusions) + assertEquals(merged, assertNotNull(delta)) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyFieldMarkExclusionTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyFieldMarkExclusionTest.kt new file mode 100644 index 000000000..52d9129f6 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyFieldMarkExclusionTest.kt @@ -0,0 +1,328 @@ +package org.opentaint.dataflow.ap.ifds.access.tree + +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.ElementAccessor +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.AnyFieldMarkExclusions +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.taint.Cleaner +import org.opentaint.dataflow.taint.PositionAccess +import org.opentaint.dataflow.taint.withSuffix +import org.opentaint.dataflow.util.Cancellation +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 + +/** + * The combination laws of the abstraction's excluded-mark annotation ([AnyFieldMarkExclusions]). + * + * A starred sanitizer cleans a fact structurally ([FinalFactAp.clean]): concrete `![m]` nodes + * below the base are deleted outright, and each abstract node picks up the residual claim that the + * mark stays excluded from whatever materializes below it later. The claim is PART OF THE NODE, so + * it travels with a prepend, is confined to its branch, is enforced when a summary delta is + * concatenated at the node, and joins by intersection when two alternatives meet at the same node. + */ +class AnyFieldMarkExclusionTest { + + private companion object { + val FIELD_RAW = FieldAccessor("Pair", "raw", "Box") + val FIELD_VAL = FieldAccessor("Pair", "val", "Box") + val FIELD_F = FieldAccessor("Box", "f", "String") + + val MARK = TaintMarkAccessor("m") + val MARK_2 = TaintMarkAccessor("n") + } + + private object UnrollStrategy : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + + private val manager = TreeApManager(UnrollStrategy, RefManager(), Cancellation()) + + private val base = AccessPathBase.This + + private fun abstractFact(): AccessTree = manager.mostAbstractFinalAp(base) as AccessTree + + private fun concreteFact(vararg accessors: Accessor): AccessTree { + var fact = manager.createFinalAp(base, org.opentaint.dataflow.ap.ifds.ExclusionSet.Empty) + for (accessor in accessors.reversed()) { + fact = fact.prependAccessor(accessor) + } + return fact as AccessTree + } + + private fun FinalFactAp.anyFieldCleaned(mark: TaintMarkAccessor = MARK): AccessTree { + val cleaner = Cleaner.Mark( + PositionAccess.Simple(base).withSuffix(listOf(AnyAccessor)), + mark, + ) + val result = clean(cleaner) + assertEquals(1, result.survivingFacts.size, "expected a surviving fact") + return result.survivingFacts.single() as AccessTree + } + + private fun merged(a: AccessTree, b: AccessTree): AccessTree = + AccessTree(manager, base, a.access.mergeAdd(b.access), a.exclusions) + + /** The caller-side content that tries to materialize below the exit fact's abstract nodes. */ + private fun deltaOf(fact: AccessTree): FinalFactAp.Delta { + val deltas = fact.delta(manager.mostAbstractInitialAp(base)) + return deltas.single { !it.isEmpty } + } + + private fun FinalFactAp.readsMarkAt(vararg accessors: Accessor): Boolean { + var node: FinalFactAp = this + for (accessor in accessors) { + node = node.readAccessor(accessor) ?: return false + } + return node.startsWithAccessor(MARK) + } + + /* ---------- the clean itself ---------- */ + + @Test + fun `any-field clean deletes concrete marks below the base and keeps the base mark`() { + // this.![m], this.f.![m] — one is the base action's territory, one is the star's + val fact = merged(concreteFact(MARK), concreteFact(FIELD_F, MARK)) + + val cleaned = fact.anyFieldCleaned() + + assertTrue(cleaned.startsWithAccessor(MARK), "the direct base mark is the base action's job") + assertFalse(cleaned.readsMarkAt(FIELD_F), "the mark below a field must be deleted") + } + + @Test + fun `any-field clean removes a fact that was only nested marks`() { + val fact = concreteFact(FIELD_F, MARK) + + val cleaner = Cleaner.Mark( + PositionAccess.Simple(base).withSuffix(listOf(AnyAccessor)), + MARK, + ) + assertTrue(fact.clean(cleaner).survivingFacts.isEmpty()) + } + + @Test + fun `any-field clean leaves an unrelated mark alone`() { + val fact = concreteFact(FIELD_F, MARK_2) + + val cleaned = fact.anyFieldCleaned(MARK) + + assertTrue( + cleaned.readAccessor(FIELD_F)?.startsWithAccessor(MARK_2) == true, + "an unrelated mark below a field must survive" + ) + } + + @Test + fun `any-field clean annotates an abstract fact instead of dropping it`() { + val cleaned = abstractFact().anyFieldCleaned() + + assertTrue(cleaned.isAbstract(), "the abstraction itself survives the clean") + assertNotNull(cleaned.access.anyFieldMarkExclusions, "the abstract node must carry the claim") + } + + /* ---------- enforcement at concat ---------- */ + + @Test + fun `a delta below the annotated base loses the mark under a field and keeps the direct mark`() { + val exit = abstractFact().anyFieldCleaned() + val delta = deltaOf(merged(concreteFact(MARK), concreteFact(FIELD_F, MARK))) + + val applied = exit.concat(FactTypeChecker.Dummy, delta) + + assertNotNull(applied, "the direct base mark keeps the fact alive") + assertTrue(applied.startsWithAccessor(MARK), "depth-1 mark is outside the star's claim") + assertFalse(applied.readsMarkAt(FIELD_F), "depth-2 mark materializing below the base must be blocked") + } + + @Test + fun `a delta that is only excluded marks does not survive the concat`() { + val exit = abstractFact().anyFieldCleaned() + val delta = deltaOf(concreteFact(FIELD_F, MARK)) + + assertNull(exit.concat(FactTypeChecker.Dummy, delta), "nothing else was attached") + } + + @Test + fun `an unrelated mark passes the annotated node untouched`() { + val exit = abstractFact().anyFieldCleaned(MARK) + val delta = deltaOf(concreteFact(FIELD_F, MARK_2)) + + val applied = exit.concat(FactTypeChecker.Dummy, delta) + + assertNotNull(applied) + assertTrue( + applied.readAccessor(FIELD_F)?.startsWithAccessor(MARK_2) == true, + "the claim is per-mark, not a blanket" + ) + } + + /* ---------- the annotation is positional ---------- */ + + @Test + fun `the annotation survives a prepend and stays confined to its branch`() { + // wrap: p.raw = b (before the clean), p.val = b (after it) — one merged exit tree + val raw = abstractFact().prependAccessor(FIELD_RAW) as AccessTree + val cleanedVal = abstractFact().anyFieldCleaned().prependAccessor(FIELD_VAL) as AccessTree + val exit = merged(raw, cleanedVal) + + val delta = deltaOf(concreteFact(FIELD_F, MARK)) + val applied = exit.concat(FactTypeChecker.Dummy, delta) + + assertNotNull(applied, "the unsanitized branch keeps the fact alive") + assertTrue(applied.readsMarkAt(FIELD_RAW, FIELD_F), "the sibling branch has no claim: the mark attaches") + assertFalse(applied.readsMarkAt(FIELD_VAL, FIELD_F), "the cleaned branch blocks the same mark") + } + + @Test + fun `an annotated node one accessor deep blocks even a direct mark`() { + // an abstract node below a field of the base: everything under it is already below one + // accessor of the cleaned base, so the claim applies from relative depth 1 + val innerCleaned = abstractFact().anyFieldCleaned().prependAccessor(FIELD_VAL) as AccessTree + val cleaned = innerCleaned.anyFieldCleaned() // the prepended tree cleaned at ITS base + + val delta = deltaOf(concreteFact(MARK)) + val applied = cleaned.readAccessor(FIELD_VAL)?.concat(FactTypeChecker.Dummy, delta) + + assertNull(applied, "a direct mark below a non-base abstract node is at base depth >= 2") + } + + /* ---------- the claim survives a summary transit ---------- */ + + @Test + fun `the empty delta carries the claim onto the transited summary's exit fact`() { + // the cleaned abstract fact passes through an unrelated callee's identity summary: + // delta vs the callee's abstract initial is empty, and the callee's exit abstraction + // continues the same object, so the claim must arrive on it + val cleanedCallerFact = abstractFact().anyFieldCleaned() + val calleeExit = abstractFact() + + val emptyDelta = cleanedCallerFact.delta(manager.mostAbstractInitialAp(base)).single { it.isEmpty } + val transited = calleeExit.concat(FactTypeChecker.Dummy, emptyDelta) as AccessTree? + + assertNotNull(transited) + assertEquals( + cleanedCallerFact.access.anyFieldMarkExclusions, + transited.access.anyFieldMarkExclusions, + "the caller's claim must ride the empty delta onto the exit abstraction" + ) + } + + @Test + fun `the transit unions the caller claim with the callee's own`() { + // the caller had cleaned m when the callee's summary, continuing the same object, + // cleaned n: both claims hold on this execution + val cleanedCallerFact = abstractFact().anyFieldCleaned(MARK) + val calleeExit = abstractFact().anyFieldCleaned(MARK_2) + + val emptyDelta = cleanedCallerFact.delta(manager.mostAbstractInitialAp(base)).single { it.isEmpty } + val transited = calleeExit.concat(FactTypeChecker.Dummy, emptyDelta) as AccessTree? + + assertNotNull(transited) + val claim = assertNotNull(transited.access.anyFieldMarkExclusions) + assertTrue(with(manager) { MARK.idx } in claim, "the caller's mark is still claimed") + assertTrue(with(manager) { MARK_2.idx } in claim, "the callee's mark is claimed too") + } + + /* ---------- joining alternative executions ---------- */ + + @Test + fun `merging cleaned and uncleaned alternatives at the same node drops the claim`() { + val cleaned = abstractFact().anyFieldCleaned() + val uncleaned = abstractFact() + val joined = merged(cleaned, uncleaned) + + assertNull(joined.access.anyFieldMarkExclusions, "the join of cleaned and uncleaned is uncleaned") + + val delta = deltaOf(concreteFact(FIELD_F, MARK)) + val applied = joined.concat(FactTypeChecker.Dummy, delta) + assertNotNull(applied) + assertTrue(applied.readsMarkAt(FIELD_F), "the uncleaned alternative's materialization must not be blocked") + } + + @Test + fun `merging two cleaned alternatives intersects their claims`() { + val cleanedBoth = abstractFact().anyFieldCleaned(MARK).anyFieldCleaned(MARK_2) + val cleanedM = abstractFact().anyFieldCleaned(MARK) + val joined = merged(cleanedBoth, cleanedM) + + val delta = deltaOf(merged(concreteFact(FIELD_F, MARK), concreteFact(FIELD_F, MARK_2))) + val applied = joined.concat(FactTypeChecker.Dummy, delta) + + assertNotNull(applied) + assertFalse(applied.readsMarkAt(FIELD_F), "m is claimed by both alternatives: blocked") + assertTrue( + applied.readAccessor(FIELD_F)?.startsWithAccessor(MARK_2) == true, + "n is claimed by one alternative only: it must survive the join" + ) + } + + @Test + fun `the join is symmetric`() { + val a = abstractFact().anyFieldCleaned(MARK).anyFieldCleaned(MARK_2) + val b = abstractFact().anyFieldCleaned(MARK) + + assertEquals( + merged(a, b).access.anyFieldMarkExclusions, + merged(b, a).access.anyFieldMarkExclusions, + "the stored claim must not depend on merge order" + ) + } + + @Test + fun `merging equal claims is identity`() { + val a = abstractFact().anyFieldCleaned() + val b = abstractFact().anyFieldCleaned() + + assertEquals(a.access.anyFieldMarkExclusions, merged(a, b).access.anyFieldMarkExclusions) + } + + /* ---------- persistence ---------- */ + + @Test + fun `the annotation survives a serialization round-trip`() { + // the sibling shape: an annotated branch and a plain one, in one tree + val fact = merged( + abstractFact().anyFieldCleaned().prependAccessor(FIELD_VAL) as AccessTree, + abstractFact().prependAccessor(FIELD_RAW) as AccessTree, + ) + + val context = object : org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext { + private val byId = mutableMapOf() + private val byAccessor = mutableMapOf() + private var next = 1L + + override fun getIdByAccessor(accessor: Accessor): Long = + byAccessor.getOrPut(accessor) { (next++).also { byId[it] = accessor } } + + override fun getAccessorById(id: Long): Accessor = byId.getValue(id) + override fun getIdByMethod(method: org.opentaint.ir.api.common.CommonMethod): Long = error("unused") + override fun getMethodById(id: Long): org.opentaint.ir.api.common.CommonMethod = error("unused") + override fun loadSummaries(method: org.opentaint.ir.api.common.CommonMethod): ByteArray? = null + override fun storeSummaries(method: org.opentaint.ir.api.common.CommonMethod, summaries: ByteArray) = Unit + override fun flush() = Unit + } + + val serializer = AccessTree.AccessNode.Serializer(manager, context) + val bytes = java.io.ByteArrayOutputStream() + with(serializer) { java.io.DataOutputStream(bytes).writeAccessNode(fact.access) } + val read = with(serializer) { + java.io.DataInputStream(java.io.ByteArrayInputStream(bytes.toByteArray())).readAccessNode() + } + + assertEquals(fact.access, read, "the annotated and the plain branch must both round-trip") + assertNotNull(read.getChild(with(manager) { FIELD_VAL.idx })?.anyFieldMarkExclusions) + assertNull(read.getChild(with(manager) { FIELD_RAW.idx })?.anyFieldMarkExclusions) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/AnyAccessorCleanTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/AnyAccessorCleanTest.kt new file mode 100644 index 000000000..234809e24 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/AnyAccessorCleanTest.kt @@ -0,0 +1,165 @@ +package org.opentaint.dataflow.taint + +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.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.tree.TreeApManager +import org.opentaint.dataflow.configuration.CommonTaintAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Characterises the shared [TaintCleanActionEvaluator.removeFinalFact] under an any-accessor position, + * which is what the Go `$*VAR` sanitizer-clean path expresses as + * `PositionAccess.Complex(base, AnyAccessor)`. + * + * Whole-object taint is stored as `base.ANY.mark`; a plain base clean uses `Simple(base)`. + */ +class AnyAccessorCleanTest { + private object UnrollStrategy : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = when (accessor) { + is ElementAccessor -> true + is FieldAccessor -> true + is ClassStaticAccessor, + is AnyAccessor, + is FinalAccessor, + is TaintMarkAccessor, + is TypeInfoAccessor, + is TypeInfoGroupAccessor -> false + is ValueAccessor -> error("unexpected accessor to unroll: $accessor") + } + } + + private val apManager = TreeApManager(UnrollStrategy, RefManager(), Cancellation()) + private val base = AccessPathBase.This + private val mark = TaintMarkAccessor("m") + private val field = FieldAccessor("A", "f", "B") + private val field2 = FieldAccessor("B", "g", "C") + + private val dummyRule = object : CommonTaintConfigurationItem {} + private val dummyAction = object : CommonTaintAction {} + + private val simple = PositionAccess.Simple(base) + private val complexAny = PositionAccess.Complex(simple, AnyAccessor) + + private fun fact(vararg accessors: Accessor): FinalFactAp { + var f = apManager.createFinalAp(base, ExclusionSet.Empty) + accessors.reversed().forEach { f = f.prependAccessor(it) } + return f + } + + /** Runs a clean and returns the surviving facts (empty == the mark was fully removed). */ + private fun clean(f: FinalFactAp, from: PositionAccess): List { + val evaluator = TaintCleanActionEvaluator() + val evc = EvaluatedCleanAction.initial(FinalFactReader(f, apManager)) + return evaluator.removeFinalFact(evc, from, mark, dummyRule, dummyAction) + .mapNotNull { it.fact?.factAp } + } + + @Test + fun `any-accessor clean removes whole-object taint`() { + // base.ANY.mark is the single fact that abstractly represents the mark on the base object + // AND every nested field it exposes. The any-accessor clean removes it entirely. + val wholeObject = fact(AnyAccessor, mark) + assertTrue(clean(wholeObject, complexAny).isEmpty(), "any-accessor clean must drop whole-object taint") + } + + @Test + fun `any-accessor clean removes a concrete nested-field mark`() { + // The whole-object ($*C) sanitizer must clear taint stored on a CONCRETE nested field, + // e.g. base.field.mark, not only the abstract base.ANY.mark. This is the shape a real + // field-taint fact takes when it reaches a starred sanitizer. + val nestedField = fact(field, mark) + assertTrue( + clean(nestedField, complexAny).isEmpty(), + "any-accessor clean must remove a concrete nested-field mark", + ) + } + + @Test + fun `containsAnyPosition (sink observation) observes a DEPTH-2 concrete field mark`() { + // The sink `sink($*o)` evaluates ContainsMarkOnAnyField via FactReader.containsAnyPosition + // (readAnyPosition) — a DIFFERENT path from the clean. This isolates read-boundedness from + // fact production: construct base.f.g.mark directly and ask if the any-field observation + // finds the mark under base at depth 2. + val deep = fact(field, field2, mark) // base.f.g.mark + val reader = FinalFactReader(deep, apManager) + val found = reader.containsAnyPosition(PositionAccess.Complex(simple, mark)) // base..mark + assertTrue(found != null, "containsAnyPosition must observe a depth-2 concrete field mark; got null") + } + + @Test + fun `containsAnyPosition observes a DEPTH-1 concrete field mark`() { + val d1 = fact(field, mark) // base.f.mark + val reader = FinalFactReader(d1, apManager) + assertTrue( + reader.containsAnyPosition(PositionAccess.Complex(simple, mark)) != null, + "containsAnyPosition must observe a depth-1 concrete field mark", + ) + } + + @Test + fun `any-accessor clean removes a DEPTH-2 concrete nested-field mark`() { + // KNOWN GAP characterization (StarDeepSink): a concrete mark buried 2 levels deep, + // base.f.g.mark. If the any-accessor read is truly unbounded-depth, the whole-object + // clean removes it; if it is depth-1-bounded, the mark survives. + val deepField = fact(field, field2, mark) + assertTrue( + clean(deepField, complexAny).isEmpty(), + "any-accessor clean must remove a depth-2 concrete nested-field mark (unbounded depth)", + ) + } + + @Test + fun `base clean removes the base mark but leaves a nested field mark`() { + // A simple position cleans the base only. + val baseMark = fact(mark) + assertTrue(clean(baseMark, simple).isEmpty(), "base clean must remove the base mark") + + val nestedField = fact(field, mark) + assertEquals( + listOf(nestedField), + clean(nestedField, simple), + "base clean must NOT reach a concrete nested-field mark", + ) + } + + @Test + fun `any-accessor clean on a base-only final fact does not crash`() { + // A summary fact that is just base.Final (no accessors, no mark) has empty start accessors. + // An any-accessor clean position (base.ANY.mark.Final) must resolve to "not contained", + // NOT throw error("Impossible") in the any-accessor read split. + val bareFinal = fact() + assertEquals( + listOf(bareFinal), + clean(bareFinal, complexAny), + "any-accessor clean must leave an unrelated base-only final fact untouched", + ) + } + + @Test + fun `the two positions are distinct - base clean does not touch whole-object, any clean does not touch a concrete base mark`() { + // Guards that exact and AnyField positions keep distinct meanings. + val baseMark = fact(mark) + assertEquals( + listOf(baseMark), + clean(baseMark, complexAny), + "any-accessor clean targets ANY-stored taint, not a concrete base-only mark", + ) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/TaintSourceActionPreconditionEvaluatorTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/TaintSourceActionPreconditionEvaluatorTest.kt new file mode 100644 index 000000000..4df9ce0e3 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/taint/TaintSourceActionPreconditionEvaluatorTest.kt @@ -0,0 +1,60 @@ +package org.opentaint.dataflow.taint + +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.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.tree.TreeApManager +import org.opentaint.dataflow.configuration.CommonTaintAssignAction +import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager +import kotlin.test.Test +import kotlin.test.assertTrue + +class TaintSourceActionPreconditionEvaluatorTest { + private val base = AccessPathBase.This + private val mark = TaintMarkAccessor("tainted") + + private object Rule : CommonTaintConfigurationItem + private object Action : CommonTaintAssignAction + + private object UnrollStrategy : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = false + } + + private fun evaluator(): TaintSourceActionPreconditionEvaluator { + val manager = TreeApManager(UnrollStrategy, RefManager(), Cancellation()) + val demandedFact = manager.mkInitialAccessPath( + PositionAccess.Simple(base).withSuffix(listOf(AnyAccessor, mark)), + ExclusionSet.Universe, + ) + return TaintSourceActionPreconditionEvaluator(InitialFactReader(demandedFact, manager)) + } + + @Test + fun `exact source cannot explain a demanded property`() { + val result = evaluator().evaluateProducedFact( + Rule, + Action, + PositionAccess.Simple(base), + mark, + ) + + assertTrue(result.isNone) + } + + @Test + fun `AnyField source can explain a demanded property`() { + val result = evaluator().evaluateProducedFact( + Rule, + Action, + PositionAccess.Simple(base).withSuffix(listOf(AnyAccessor)), + mark, + ) + + assertTrue(result.isSome) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt index 83066c2ef..ce11fe4ed 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt @@ -294,13 +294,16 @@ object GoFlowFunctionUtils { is PositionWithAccess -> PositionAccess.Complex(base.resolvePosAccess(), access.resolvePosAccess()) } - fun Position.Simple.resolvePosAccess(): PositionAccess.Simple { - val base = when (this) { - is Position.Argument -> AccessPathBase.Argument(index) - is Position.Result -> AccessPathBase.Return - is Position.This -> AccessPathBase.This - } - return PositionAccess.Simple(base) + fun Position.Simple.resolvePosAccess(): PositionAccess = when (this) { + is Position.Argument -> PositionAccess.Simple(AccessPathBase.Argument(index)) + is Position.Result -> PositionAccess.Simple(AccessPathBase.Return) + is Position.This -> PositionAccess.Simple(AccessPathBase.This) + // The state-var global slot: base ClassStatic + a ClassStaticAccessor carrying the + // name, exactly how Go models global variables (mirrors the JVM resolveAp). + is Position.ClassStatic -> PositionAccess.Complex( + PositionAccess.Simple(AccessPathBase.ClassStatic), + ClassStaticAccessor(className) + ) } fun PositionAccessor.resolvePosAccess(): Accessor = when (this) { diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt index 7876cfb30..b08ee4ed1 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt @@ -1,5 +1,7 @@ package org.opentaint.dataflow.go.analysis +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.AnyAccessor import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp @@ -13,6 +15,7 @@ import org.opentaint.dataflow.go.rules.TaintRule import org.opentaint.dataflow.go.signature import org.opentaint.dataflow.taint.EvaluatedCleanAction import org.opentaint.dataflow.taint.FinalFactReader +import org.opentaint.dataflow.taint.PositionAccess import org.opentaint.dataflow.taint.TaintCleanActionEvaluator import org.opentaint.dataflow.taint.applyCleanerActions import org.opentaint.ir.go.inst.GoIRInst @@ -30,9 +33,14 @@ class GoCallRuleBasedSummaryRewriter( private val callSignature: GoFunctionSignature? get() = callExpr.signature() + private data class CleanPosition( + val pos: Position, + val onAnyAccessor: Boolean, + ) + private data class UserRuleDefinedAction( val rule: TaintRule, - val positions: Set, + val positions: Set, val controlledMarks: Set ) @@ -46,7 +54,7 @@ class GoCallRuleBasedSummaryRewriter( if (sourceRuleWithCond.condition.isFalse) continue - val positions = sourceRule.actionsAfter.mapTo(hashSetOf()) { it.rawPosition() } + val positions = sourceRule.actionsAfter.mapTo(hashSetOf()) { CleanPosition(it.rawPosition(), onAnyAccessor = false) } result += UserRuleDefinedAction(sourceRule, positions, ruleInfo.relevantTaintMarks) } @@ -56,13 +64,68 @@ class GoCallRuleBasedSummaryRewriter( if (cleanRuleWithCond.condition.isFalse) continue - val positions = cleanRule.actionsAfter.filterIsInstance().mapTo(hashSetOf()) { it.pos } + val positions = cleanRule.actionsAfter.filterIsInstance() + .mapTo(hashSetOf()) { CleanPosition(it.pos, it.onAnyAccessor) } result += UserRuleDefinedAction(cleanRule, positions, ruleInfo.relevantTaintMarks) } result } + // Only the sanitizer clean actions (no sources) — used at call-to-start where the concrete + // argument-keyed fact exists but the result position does not. + private val cleanOnlyActions: List by lazy { + val signature = callSignature ?: return@lazy emptyList() + + buildList { + for (cleanRuleWithCond in config.allRelevantCleanRulesForCallStatement(signature, statement, callExpr, returnValue)) { + val cleanRule = cleanRuleWithCond.rule + val ruleInfo = cleanRule.info as? GoUserDefinedRuleInfo ?: continue + if (cleanRuleWithCond.condition.isFalse) continue + + val positions = cleanRule.actionsAfter.filterIsInstance() + .mapTo(hashSetOf()) { CleanPosition(it.pos, it.onAnyAccessor) } + add(UserRuleDefinedAction(cleanRule, positions, ruleInfo.relevantTaintMarks)) + } + } + } + + /** + * Applies ONLY the sanitizer clean actions to a concrete call-to-start fact, mirroring the JVM's + * call-to-start cleaner ([JIRMethodCallFlowFunction.applyCleanersOrCallToStart]). The Go summary + * handler only cleans abstract summary edges, which carry no concrete mark, so a resolved + * pass-through sanitizer (`Clean($C) { return $C }`) never cleared the field taint flowing through. + * + * [fact] is in the caller frame with [startBase] the callee entry base; the clean positions + * (e.g. `Argument(0)`) resolve against the call frame, so the fact is rebased onto [startBase] + * for the clean and back afterwards. Returns the surviving facts (empty == fully sanitized). + */ + fun cleanCallToStartFact(fact: FinalFactAp, startBase: AccessPathBase): List { + if (cleanOnlyActions.isEmpty()) return listOf(fact) + + val originalBase = fact.base + val callFrameFact = fact.rebase(startBase) + val startFactReader = FinalFactReader(callFrameFact, apManager) + val cleanEvaluator = TaintCleanActionEvaluator() + + val cleanedFact = cleanOnlyActions.applyCleanerActions( + evalAction = { f, rule, action -> + val base = action.pos.resolvePosAccess() + val pos = if (action.onAnyAccessor) PositionAccess.Complex(base, AnyAccessor) else base + cleanEvaluator.removeFinalFact(f, pos, TaintMarkAccessor(action.mark), rule, action) + }, + itemRule = { it.rule }, + itemActions = { action -> + action.controlledMarks.flatMap { mark -> + action.positions.map { RemoveMark(mark, it.pos, it.onAnyAccessor) } + } + }, + initial = EvaluatedCleanAction.initial(startFactReader) + ) + + return cleanedFact.mapNotNull { it.fact?.factAp?.rebase(originalBase) } + } + fun rewriteSummaryFact(fact: FinalFactAp): List> { val startFactReader = FinalFactReader(fact, apManager) @@ -70,13 +133,14 @@ class GoCallRuleBasedSummaryRewriter( val cleanedFact = userRuleDefinedActions.applyCleanerActions( evalAction = { f, rule, action -> - val pos = action.pos.resolvePosAccess() + val base = action.pos.resolvePosAccess() + val pos = if (action.onAnyAccessor) PositionAccess.Complex(base, AnyAccessor) else base cleanEvaluator.removeFinalFact(f, pos, TaintMarkAccessor(action.mark), rule, action) }, itemRule = { it.rule }, itemActions = { action -> action.controlledMarks.flatMap { mark -> - action.positions.map { RemoveMark(mark, it) } + action.positions.map { RemoveMark(mark, it.pos, it.onAnyAccessor) } } }, initial = EvaluatedCleanAction.initial(startFactReader) diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallFlowFunction.kt index 06f33f17a..64030e788 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallFlowFunction.kt @@ -3,6 +3,7 @@ package org.opentaint.dataflow.go.analysis import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.SideEffectKind import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp @@ -113,7 +114,12 @@ class GoMethodCallFlowFunction( ) factAp.mapCall2Start { fact, startBase -> - addCallToStart(factReader, fact, startBase, TraceInfo.Flow) + // Apply the sanitizer clean at call-to-start on the concrete argument-keyed fact. The Go + // summary handler only cleans abstract summary edges (no concrete mark), so a resolved + // pass-through sanitizer would otherwise let the field taint flow through uncleaned. + for (cleanedFact in summaryRewriter.cleanCallToStartFact(fact, startBase)) { + addCallToStart(factReader, cleanedFact, startBase, TraceInfo.Flow) + } } if (factReader.hasRefinement) { @@ -176,8 +182,10 @@ class GoMethodCallFlowFunction( override fun propagateUnresolvedCallFact( factAp: FinalFactAp, + initialFacts: Set, addCallToReturn: (FinalFactReader, FinalFactAp, TraceInfo?) -> Unit, - addSideEffectRequirement: (FinalFactReader) -> Unit + addSideEffectRequirement: (FinalFactReader) -> Unit, + addSideEffect: (InitialFactAp, SideEffectKind) -> Unit, ) { propagateDefault(factAp, addCallToReturn) diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallTaintUtil.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallTaintUtil.kt index 81da32ce2..3a1b26d79 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallTaintUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoMethodCallTaintUtil.kt @@ -1,7 +1,5 @@ package org.opentaint.dataflow.go.analysis -import org.opentaint.dataflow.ap.ifds.AccessPathBase -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.TaintMarkAccessor @@ -17,10 +15,7 @@ import org.opentaint.dataflow.go.GoMethodCallFactMapper.mapMethodExitToReturnFlo import org.opentaint.dataflow.go.rules.GoAssignAction import org.opentaint.dataflow.go.rules.GoRuleCondition import org.opentaint.dataflow.go.rules.TaintRule -import org.opentaint.dataflow.taint.FactReader import org.opentaint.dataflow.taint.FinalFactReader -import org.opentaint.dataflow.taint.FinalFactReaderWithPrefix -import org.opentaint.dataflow.taint.PositionAccess import org.opentaint.dataflow.taint.TaintSourceActionEvaluator import org.opentaint.dataflow.taint.TaintUtil import org.opentaint.ir.go.inst.GoIRInst @@ -79,16 +74,6 @@ class GoMethodCallTaintUtil( return readers } - override fun patchSinkConditionFactReader(factReaders: List): List { - val elementWrappedReaders = factReaders.mapNotNull { reader -> - val base = reader.factAp.base as? AccessPathBase.Argument ?: return@mapNotNull null - val elementPosition = PositionAccess.Complex(PositionAccess.Simple(base), ElementAccessor) - if (!reader.containsPosition(elementPosition)) return@mapNotNull null - FinalFactReaderWithPrefix(reader, ElementAccessor) - } - return factReaders + elementWrappedReaders - } - override fun handleReachedSink( rule: TaintRule.Sink, factReader: FinalFactReader?, diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt index b7153d831..b46d30f68 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt @@ -57,6 +57,7 @@ class GoBasicAtomEvaluator( is Position.Argument -> callExpr.explicitArgs.getOrNull(pos.index) is Position.Result -> returnValue is Position.This -> callExpr.effectiveReceiver + is Position.ClassStatic -> null } private inline fun cmpConstant( diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt index 8ca0a6318..6758a677c 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt @@ -33,9 +33,19 @@ private fun GoSerializedCondition.resolveImpl(signature: GoFunctionSignature): C is GoSerializedCondition.Or -> mkOr(anyOf.map { it.resolveImpl(signature) }) is GoSerializedCondition.Not -> CommonCondition.Not(not.resolveImpl(signature)) - is GoSerializedCondition.ContainsMark -> pos.resolveAny(signature, PositionBaseWithModifiers::resolve) { - GoRuleCondition.ContainsMark(it, tainted) - } + is GoSerializedCondition.ContainsMark -> mkOr( + listOf( + pos.resolveAny(signature, PositionBaseWithModifiers::resolve) { + GoRuleCondition.ContainsMark(it, tainted) + }, + // Star-model replacement for the removed runtime element reader + // (GoMethodCallTaintUtil.patchSinkConditionFactReader): a position also observes + // element (`arg[*]`, incl. variadic `...T`) taint via the recursive any-accessor check. + pos.resolveAny(signature, PositionBaseWithModifiers::resolve) { + GoRuleCondition.ContainsMarkOnAnyAccessor(it, tainted) + } + ) + ) is GoSerializedCondition.ContainsMarkOnAnyAccessor -> pos.resolveAny(signature, PositionBaseWithModifiers::resolve) { GoRuleCondition.ContainsMarkOnAnyAccessor(it, tainted) @@ -109,7 +119,7 @@ fun PositionBase.resolve(signature: GoFunctionSignature): List } } - is PositionBase.ClassStatic -> error("Unused") + is PositionBase.ClassStatic -> listOf(Position.ClassStatic(className)) is PositionBase.Result -> listOf(Position.Result) is PositionBase.This -> if (signature.hasReceiver) listOf(Position.This) else emptyList() } @@ -141,6 +151,7 @@ fun PositionBaseWithModifiers.resolve(signature: GoFunctionSignature): List = when (pos) { is Position.Argument -> listOfNotNull(paramTypes.getOrNull(pos.index)) + is Position.ClassStatic -> emptyList() is Position.Result -> { val types = mutableListOf(resultType) if (resultType is GoIRTupleType) { diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt index 4e37b904b..1bbd1b5b4 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt @@ -19,6 +19,7 @@ data class CopyData( data class RemoveMark( val mark: String, val pos: Position, + val onAnyAccessor: Boolean = false, ) : GoTaintAction data class RemoveAllMarks( diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt index 03c38dfdf..97e9e2a44 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt @@ -277,7 +277,11 @@ class GoTaintConfiguration : GoTaintRulesProvider { private fun GoSerializedCleanAction.toTaintAction(signature: GoFunctionSignature): List = pos.resolve(signature).map { val kind = taintKind - if (kind == null) RemoveAllMarks(it) else RemoveMark(kind, it) + when { + kind == null -> RemoveAllMarks(it) + this is GoSerializedCleanAction.AnyAccessor -> RemoveMark(kind, it, onAnyAccessor = true) + else -> RemoveMark(kind, it) + } } private fun generateRuleId(rule: GoSerializedRule.Sink): String { diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt index 2359a5586..cc0435527 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt @@ -12,6 +12,11 @@ sealed interface Position { data object Result : Simple { override fun toString(): String = javaClass.simpleName } + + // A named global slot (the querylang state-var mechanism); mirrors the JVM + // Position.ClassStatic and resolves to the ClassStatic access-path base with a + // ClassStaticAccessor carrying the name. + data class ClassStatic(val className: String) : Simple } sealed interface PositionAccessor { diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/trace/GoMethodCallPrecondition.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/trace/GoMethodCallPrecondition.kt index 0c7f63d37..4db575ed8 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/trace/GoMethodCallPrecondition.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/trace/GoMethodCallPrecondition.kt @@ -139,6 +139,9 @@ class GoMethodCallPrecondition( rule.rule.actionsAfter, sourcePreconditionEvaluator = sourcePreconditionEvaluator, evalAction = { r, a -> evaluate(r, a, a.resolvePosAccess(), TaintMarkAccessor(a.mark)) }, + evalProducedFact = { r, a -> + evaluateProducedFact(r, a, a.resolvePosAccess(), TaintMarkAccessor(a.mark)) + }, ) } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRFactTypeChecker.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRFactTypeChecker.kt index fe2a05424..c76a779b3 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRFactTypeChecker.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRFactTypeChecker.kt @@ -2,7 +2,6 @@ package org.opentaint.dataflow.jvm.ap.ifds import it.unimi.dsi.fastutil.longs.LongLongImmutablePair import it.unimi.dsi.fastutil.longs.LongLongPair -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 @@ -32,7 +31,6 @@ import org.opentaint.ir.api.jvm.JIRRefType import org.opentaint.ir.api.jvm.JIRType import org.opentaint.ir.api.jvm.JIRTypeVariable import org.opentaint.ir.api.jvm.JIRUnboundWildcard -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.ext.ifArrayGetElementType import org.opentaint.ir.api.jvm.ext.isAssignable import org.opentaint.ir.api.jvm.ext.isSubClassOf @@ -184,17 +182,6 @@ class JIRFactTypeChecker(private val cp: JIRClasspath) : FactTypeChecker { return AccessorCompatibilityFilter(actualType) } - fun callArgumentMayBeArray(call: JIRCallExpr, arg: AccessPathBase.Argument): Boolean { - val argument = call.args.getOrNull(arg.idx) ?: return false - val argType = argument.type - return argType.mayBeArray() - } - - fun JIRType.mayBeArray(): Boolean { - if (this !is JIRRefType) return false - return typeMayBeArrayType(this) - } - private fun accessorActualType(accessPath: List): JIRType? { val accessor = accessPath.lastOrNull() ?: return null return when (accessor) { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRMarkAwareConditionRewriter.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRMarkAwareConditionRewriter.kt index 5f62ce238..45b948437 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRMarkAwareConditionRewriter.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRMarkAwareConditionRewriter.kt @@ -3,7 +3,9 @@ package org.opentaint.dataflow.jvm.ap.ifds import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.configuration.jvm.ContainsMark import org.opentaint.dataflow.configuration.jvm.JirCondition +import org.opentaint.dataflow.configuration.jvm.PositionAccessor import org.opentaint.dataflow.configuration.jvm.PositionResolver +import org.opentaint.dataflow.configuration.jvm.PositionWithAccess import org.opentaint.dataflow.jvm.ap.ifds.analysis.JIRMethodAnalysisContext import org.opentaint.dataflow.jvm.ap.ifds.taint.ContainsMarkOnAnyField import org.opentaint.dataflow.jvm.ap.ifds.taint.JIRBasicAtomEvaluator @@ -39,15 +41,39 @@ class JIRMarkAwareConditionRewriter( } private fun rewriteAtom(atom: JirCondition, evaluator: JIRBasicAtomEvaluator): ExprOrConstant { - if (atom is ContainsMark) { - return ExprOrConstant(TaintMarkAwareConditionExpr.ContainsMarkLiteral(atom.position.resolveAp(), TaintMarkAccessor(atom.mark.name), negated = false)) + val normalizedAtom = normalizeTrailingAnyField(atom) + + if (normalizedAtom is ContainsMark) { + return ExprOrConstant(TaintMarkAwareConditionExpr.ContainsMarkLiteral(normalizedAtom.position.resolveAp(), TaintMarkAccessor(normalizedAtom.mark.name), negated = false)) } - if (atom is ContainsMarkOnAnyField) { - return ExprOrConstant(TaintMarkAwareConditionExpr.ContainsMarkOnAnyAccessorLiteral(atom.position.resolveAp(), TaintMarkAccessor(atom.mark.name), negated = false)) + if (normalizedAtom is ContainsMarkOnAnyField) { + return ExprOrConstant(TaintMarkAwareConditionExpr.ContainsMarkOnAnyAccessorLiteral(normalizedAtom.position.resolveAp(), TaintMarkAccessor(normalizedAtom.mark.name), negated = false)) } val result = atom.accept(evaluator) return if (result) trueExpr else falseExpr } + + /** + * A trailing any-field modifier (`arg(0).*` in a serialized condition) asks whether the mark + * sits anywhere at or below the position, which is exactly what [ContainsMarkOnAnyField] means, + * so the serialized form is normalised into it and lowered by the single + * `ContainsMarkOnAnyAccessorLiteral` construction site above. Lowering it to a plain + * ContainsMarkLiteral over an AnyAccessor query instead silently matches nothing: an abstract + * fact reports its read mismatch without an accessor, so the demand-driven refinement that + * unfolds the fact never fires. + * + * Only a TRAILING modifier is normalised: the any-field-ness is carried by the condition type, + * so the position handed to [ContainsMarkOnAnyField] is the base. A non-trailing any field + * (`arg(0).*.f`) keeps its accessor chain and falls through to the ContainsMarkLiteral path. + */ + private fun normalizeTrailingAnyField(atom: JirCondition): JirCondition { + if (atom !is ContainsMark) return atom + + val position = atom.position + if (position !is PositionWithAccess || position.access !is PositionAccessor.AnyFieldAccessor) return atom + + return ContainsMarkOnAnyField(position.base, atom.mark) + } } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRSummariesFeature.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRSummariesFeature.kt index e89922df0..be4e636bd 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRSummariesFeature.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRSummariesFeature.kt @@ -172,6 +172,7 @@ class JIRSummariesFeature( fieldNameId = accessorEntity.get("fieldNameId"), fieldTypeId = accessorEntity.get("fieldTypeId"), taintMarkId = accessorEntity.get("taintMarkId"), + taintMarkDeep = accessorEntity.get("taintMarkDeep"), staticTypeNameId = accessorEntity.get("staticTypeNameId"), typeInfoTypeNameId = accessorEntity.get("typeInfoTypeNameId"), ) @@ -234,6 +235,7 @@ class JIRSummariesFeature( val taintMarkId = accessor.mark.asSymbolId(interner) val accessorId = jIRdb.persistence.read { context -> context.txn.find(ACCESSOR_IDS_TYPE, "taintMarkId", taintMarkId) + .filter { (it.get("taintMarkDeep") ?: 0L) == 0L } .singleOrNull() ?.get("id") } @@ -243,6 +245,7 @@ class JIRSummariesFeature( } } + is ClassStaticAccessor -> accessorToIdCache.computeIfAbsent(accessor) { val staticTypeNameId = accessor.typeName.asSymbolId(interner) val accessorId = jIRdb.persistence.read { context -> @@ -276,6 +279,10 @@ class JIRSummariesFeature( val summaryEntry = context.txn .find(METHOD_SUMMARIES_TYPE, "methodId", methodId) .filter { it.get("apModeId") == apModeId } + // Entities written before the format version existed have no property here + // and are rejected rather than misread (the tree node format changed when + // abstraction annotations replaced flat deep exclusions). + .filter { it.get("formatVersion") == SUMMARIES_FORMAT_VERSION } .singleOrNull() summaryEntry?.getRawBlob("summaries") } ?: ByteArray(0) @@ -302,12 +309,14 @@ class JIRSummariesFeature( if (oldEntity != null) { if (updateExistingSummaries) { + oldEntity["formatVersion"] = SUMMARIES_FORMAT_VERSION oldEntity.setRawBlob("summaries", summaries) } } else { context.txn.newEntity(METHOD_SUMMARIES_TYPE).also { summariesEntity -> summariesEntity["methodId"] = methodId summariesEntity["apModeId"] = apModeId + summariesEntity["formatVersion"] = SUMMARIES_FORMAT_VERSION summariesEntity.setRawBlob("summaries", summaries) } } @@ -389,6 +398,7 @@ class JIRSummariesFeature( val fieldNameId: Long?, val fieldTypeId: Long?, val taintMarkId: Long?, + val taintMarkDeep: Long?, val staticTypeNameId: Long?, val typeInfoTypeNameId: Long?, ) @@ -398,6 +408,12 @@ class JIRSummariesFeature( private const val ACCESSOR_IDS_TYPE = "AccessorIds" private const val METHOD_SUMMARIES_TYPE = "MethodSummaries" + /** + * Bump when the serialized summary format changes incompatibly. Version 3 stores + * demand exclusions and AnyField mark exclusions independently. + */ + private const val SUMMARIES_FORMAT_VERSION = 3 + private const val ANY_ACCESSOR_ID = 0L private const val FINAL_ACCESSOR_ID = 1L private const val ELEMENT_ACCESSOR_ID = 2L @@ -405,4 +421,4 @@ class JIRSummariesFeature( private const val TYPE_INFO_GROUP_ACCESSOR_ID = 4L private const val MAX_RESERVED_ACCESSOR_ID = 4L } -} \ 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/JIRMethodCallFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt index 81c6b6ee0..7594ce47d 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 @@ -2,6 +2,7 @@ package org.opentaint.dataflow.jvm.ap.ifds.analysis import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.SideEffectKind import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp @@ -120,7 +121,8 @@ class JIRMethodCallFlowFunction( final.forEachSourceFactWithAliases { addUnchecked(CallToReturnNonDistributiveFact(initial, it, trace)) } - } + }, + markAfterAnyFieldResolver = markAfterAnyFieldResolver, ) JIRMethodCallFactMapper.mapMethodCallToStartFlowFact( @@ -133,7 +135,8 @@ class JIRMethodCallFlowFunction( ) { callerFact, startFactBase -> applyCleanersOrCallToStart( factReader, callerFact, startFactBase, - addCallToReturn, addCallToStart, addUnchecked + addCallToReturn, addCallToStart, addUnchecked, + markAfterAnyFieldResolver ) } @@ -149,6 +152,7 @@ class JIRMethodCallFlowFunction( addCallToReturn: (FinalFactReader, FinalFactAp, TraceInfo) -> Unit, addCallToStart: (factReader: FinalFactReader, callerFactAp: FinalFactAp, startFactBase: AccessPathBase, TraceInfo) -> Unit, addCallToReturnUnchecked: (MethodCallFlowFunction.CallFact) -> Unit, + markAfterAnyFieldResolver: FactWithMarkAfterAnyAccessorResolver?, ) { val method = callExpr.callee @@ -157,7 +161,7 @@ class JIRMethodCallFlowFunction( val conditionEvaluator = TaintFactAwareConditionEvaluator( listOf(conditionFactReader), - markAfterAnyAccessorResolver = null // we don't expect such marks in pass rules + markAfterAnyAccessorResolver = markAfterAnyFieldResolver ) val cleaner = JIRTaintCleanActionEvaluator(typeResolver) @@ -238,6 +242,7 @@ class JIRMethodCallFlowFunction( createFinalFact: (FinalFactAp, TraceInfo) -> Unit, createEdge: (InitialFactAp, FinalFactAp, TraceInfo) -> Unit, createNDEdge: (Set, FinalFactAp, TraceInfo) -> Unit, + markAfterAnyFieldResolver: FactWithMarkAfterAnyAccessorResolver? = null, ) { val sourceRules = taintCtx.sourceRulesForCallStatement(statement, callExpr, returnValue, factReader?.factAp) if (sourceRules.isEmpty()) return @@ -245,20 +250,27 @@ class JIRMethodCallFlowFunction( val taintUtil = JIRMethodCallTaintUtil(apManager, statement, callExpr, analysisContext, generateTrace) taintUtil.applySourceRules( sourceRules, initialFacts, factReader, exclusion, - createFinalFact, createEdge, createNDEdge + createFinalFact, createEdge, createNDEdge, + markAfterAnyFieldResolver ) } override fun propagateUnresolvedCallFact( factAp: FinalFactAp, + initialFacts: Set, addCallToReturn: (FinalFactReader, FinalFactAp, TraceInfo?) -> Unit, - addSideEffectRequirement: (FinalFactReader) -> Unit + addSideEffectRequirement: (FinalFactReader) -> Unit, + addSideEffect: (InitialFactAp, SideEffectKind) -> Unit, ) { val factReader = FinalFactReader(factAp, apManager) unresolvedCallDefaultFactPropagation(factAp, addCallToReturn) val method = callExpr.callee + + val markAfterAnyFieldResolver = createMarkAfterAccessorResolver( + analysisContext.methodEntryPoint, initialFacts, addSideEffect + ) JIRMethodCallFactMapper.mapMethodCallToStartFlowFact( statement, callee = method, @@ -271,7 +283,7 @@ class JIRMethodCallFlowFunction( val conditionEvaluator = TaintFactAwareConditionEvaluator( listOf(passFactReader), - markAfterAnyAccessorResolver = null // we don't expect such marks in pass rules + markAfterAnyAccessorResolver = markAfterAnyFieldResolver ) val passEvaluator = TaintPassActionEvaluator( 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..2c979ae18 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 @@ -3,6 +3,7 @@ package org.opentaint.dataflow.jvm.ap.ifds.analysis import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.configuration.TaintCleanReach import org.opentaint.dataflow.configuration.jvm.Position import org.opentaint.dataflow.configuration.jvm.RemoveMark import org.opentaint.dataflow.configuration.jvm.TaintConfigurationItem @@ -100,7 +101,9 @@ class JIRMethodCallRuleBasedSummaryRewriter( itemRule = { it.rule }, itemActions = { ruleDefinedAction -> val taintMark = TaintMark(mark) - ruleDefinedAction.positions.map { RemoveMark(taintMark, it) } + ruleDefinedAction.positions.map { + RemoveMark(taintMark, it, TaintCleanReach.ExactAndAnyField) + } }, initial = current ) 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..de1b779ba 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 @@ -591,7 +591,9 @@ class JIRMethodSequentFlowFunction( accessor: Accessor, propagateFactWithAccessorExclude: (FinalFactAp, Accessor) -> Unit ) { - val abstractAp = factAp.abstractOnly() + // abstractPart, not createAbstractAp: the partition must keep representation state + // attached to the abstraction or the store can resurrect cleaned content. + val abstractAp = factAp.abstractPart() propagateFactWithAccessorExclude(abstractAp, accessor) analysisContext.aliasAnalysis?.forEachAliasAtStatement(currentInst, abstractAp) { aliased -> 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..f97bac092 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 @@ -1,7 +1,5 @@ package org.opentaint.dataflow.jvm.ap.ifds.taint -import org.opentaint.dataflow.ap.ifds.AccessPathBase -import org.opentaint.dataflow.ap.ifds.ElementAccessor import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp @@ -16,10 +14,7 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallFactMapper import org.opentaint.dataflow.jvm.ap.ifds.TaintConfigUtils.accept import org.opentaint.dataflow.jvm.ap.ifds.analysis.JIRMethodAnalysisContext import org.opentaint.dataflow.jvm.util.callee -import org.opentaint.dataflow.taint.FactReader import org.opentaint.dataflow.taint.FinalFactReader -import org.opentaint.dataflow.taint.FinalFactReaderWithPrefix -import org.opentaint.dataflow.taint.PositionAccess import org.opentaint.dataflow.taint.TaintSourceActionEvaluator import org.opentaint.dataflow.taint.TaintUtil import org.opentaint.ir.api.jvm.cfg.JIRCallExpr @@ -183,25 +178,6 @@ class JIRMethodCallTaintUtil( JIRMethodCallFactMapper.mapMethodExitToReturnFlowFact(statement, this) .singleOrNull() - override fun patchSinkConditionFactReader(factReaders: List): List { - val arrayElementFactReaders = factReaders.arrayElementConditionReaders(callExpr) - return factReaders + arrayElementFactReaders - } - - private fun List.arrayElementConditionReaders(callExpr: JIRCallExpr): List = - mapNotNull { - val base = it.factAp.base as? AccessPathBase.Argument ?: return@mapNotNull null - - if (!analysisContext.factTypeChecker.callArgumentMayBeArray(callExpr, base)) { - return@mapNotNull null - } - - val arrayElementPosition = PositionAccess.Complex(PositionAccess.Simple(base), ElementAccessor) - if (!it.containsPosition(arrayElementPosition)) return@mapNotNull null - - FinalFactReaderWithPrefix(it, ElementAccessor) - } - private inline fun storeInfo(body: () -> Unit) { if (generateTrace) return body() diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt index 33bc83ca7..6f2deeda5 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt @@ -47,7 +47,14 @@ class JIRTaintCleanActionEvaluator( ): List { val variable = action.position.resolveAp() val mark = TaintMarkAccessor(action.mark.name) - val cleaned = evaluator.removeFinalFact(initialFact, variable, mark, rule, action) + val cleaned = evaluator.removeFinalFact( + initialFact, + variable, + mark, + rule, + action, + action.reach, + ) val positionType = positionTypeResolver.resolve(variable) if (positionType?.typeName != STRING) { @@ -56,7 +63,7 @@ class JIRTaintCleanActionEvaluator( val stringBytesVar = PositionWithAccess(action.position, stringBytes).resolveAp() return cleaned.flatMap { f -> - evaluator.removeFinalFact(f, stringBytesVar, mark, rule, action) + evaluator.removeFinalFact(f, stringBytesVar, mark, rule, action, action.reach) } } 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..4ad88704e 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 @@ -153,6 +153,9 @@ class JIRMethodCallPrecondition( rule.rule.actionsAfter, sourcePreconditionEvaluator = sourcePreconditionEvaluator, evalAction = { r, a -> evaluate(r, a, a.position.resolveAp(), TaintMarkAccessor(a.mark.name)) }, + evalProducedFact = { r, a -> + evaluateProducedFact(r, a, a.position.resolveAp(), TaintMarkAccessor(a.mark.name)) + }, ) } 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..3cdac539c 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 @@ -319,6 +319,9 @@ class JIRMethodSequentPrecondition( evaluateSourceRulePrecondition( ruleWithCond, ruleWithCond.rule.actionsAfter, sourcePreconditionEvaluator, evalAction = { r, a -> evaluate(r, a, a.position.resolveAp(), TaintMarkAccessor(a.mark.name)) }, + evalProducedFact = { r, a -> + evaluateProducedFact(r, a, a.position.resolveAp(), TaintMarkAccessor(a.mark.name)) + }, mkSource = { r, a -> val src = TaintRulePrecondition.Source(r, a) this += MethodSequentPrecondition.SequentSource(fact, src) diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt index aefa6cc53..65d1ef1e9 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt @@ -39,6 +39,69 @@ class DSUAliasAnalysisInvalidateOuterHeapAliasesTest { private fun State.invalidate(builder: StateBuilder, start: Set): State = with(analysis) { invalidateOuterHeapAliases(builder.infoIds(start)) } + /** + * KNOWN FALSE NEGATIVE: invalidation drops a still-live heap alias entirely. + * + * Shape of `o = build(); x = o.f; ; x.v = tainted; sink(o)`: o's group holds an + * outer element (the call return), and x holds the value loaded from the slot. The opaque call + * may reassign `o.f`, so the LIVE slot link must break — a later load of `o.f` must not rejoin + * x's set. But the pair itself should survive in some form, so a store through `x` can still be + * rebased onto `o.f` as a may-alias. + * + * The DSU cannot represent a singleton set, so breaking the link removes the whole pair and the + * `%tmp ~ o.f` relation is lost. The tainted store through the temp is never rebased onto + * `o.f.v`, which is the depth-2 false negative parked as + * `taint.StarDeepSink.KnownFnDepth2`. Depth >= 3 escapes it only by accident: the intermediate + * temps are dead at the call, so dead-local cleanup has already turned the chain into orphaned + * elements that the invalidation cascade cannot see through. + * + * This test pins the current, lossy behaviour. When the underlying representation gains a way + * to keep the relation (without letting a post-call load rejoin the pre-call set), flip the + * expectation to keep `x ~ o.f` and unpark `KnownFnDepth2`. + */ + @Test + fun invalidateDropsLiveHeapAliasLosingPathRelation() { + val builder = fillState { + val o = local(0) + val outer = outerThis() + merge(setOf(o, outer)) + val x = local(2) + val f = fieldAlias(o, "f", isImmutable = false) + merge(setOf(x, f)) + } + val state = builder.build() + + val result = state.invalidate(builder, emptySet()) + + val expected = buildState { + // x's set is gone entirely: the o.f element was removed and x was never merged into + // the DSU on its own, so nothing records that x held the value of o.f. + val o2 = local(0) + val outer2 = outerThis() + merge(setOf(o2, outer2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateIsIdempotent() { + val builder = fillState { + val o = local(0) + val outer = outerThis() + merge(setOf(o, outer)) + val x = local(2) + val f = fieldAlias(o, "f", isImmutable = false) + merge(setOf(x, f)) + } + val state = builder.build() + + val once = state.invalidate(builder, emptySet()) + val twice = once.invalidate(builder, emptySet()) + + assertEquals(once, twice) + } + @Test fun invalidateEmptyStartSetIsNoop() { val builder = fillState { diff --git a/core/opentaint-go-querylang/grammar/semgrep-extensions.patch b/core/opentaint-go-querylang/grammar/semgrep-extensions.patch index 01e47f202..07bdb9d70 100644 --- a/core/opentaint-go-querylang/grammar/semgrep-extensions.patch +++ b/core/opentaint-go-querylang/grammar/semgrep-extensions.patch @@ -1,15 +1,16 @@ --- a/GoLexer.g4 +++ b/GoLexer.g4 -@@ -69,6 +69,15 @@ +@@ -69,6 +69,16 @@ TYPE : 'type'; VAR : 'var'; - + + +// --- Semgrep extensions --- +LDOTS : '<...'; +RDOTS : '...>' -> mode(NLSEMI); +METAVAR_ELLIPSIS : '$...' [A-Z_] [A-Z_0-9]* -> mode(NLSEMI); +ANONYMOUS_METAVAR : '$_' -> mode(NLSEMI); ++METAVAR_STAR_IDENT : '$*' [A-Z_] [A-Z_0-9]* -> mode(NLSEMI); +METAVAR_IDENT : '$' [A-Z_] [A-Z_0-9]* -> mode(NLSEMI); +// --- end semgrep extensions --- + @@ -45,15 +46,16 @@ // Hidden tokens -@@ -210,6 +229,16 @@ +@@ -210,6 +230,17 @@ fragment UNICODE_LETTER: [\p{L}]; - + mode NLSEMI; +// --- Semgrep extensions in NLSEMI mode --- +LDOTS_NLSEMI : '<...' -> type(LDOTS), mode(DEFAULT_MODE); +RDOTS_NLSEMI : '...>' -> type(RDOTS); +METAVAR_ELLIPSIS_NLSEMI : '$...' [A-Z_] [A-Z_0-9]* -> type(METAVAR_ELLIPSIS); +ANONYMOUS_METAVAR_NLSEMI : '$_' -> type(ANONYMOUS_METAVAR); ++METAVAR_STAR_IDENT_NLSEMI : '$*' [A-Z_] [A-Z_0-9]* -> type(METAVAR_STAR_IDENT); +METAVAR_IDENT_NLSEMI : '$' [A-Z_] [A-Z_0-9]* -> type(METAVAR_IDENT); +METAVAR_LITERAL_NLSEMI : '"' '$' [A-Z_] [A-Z_0-9]* '"' -> type(METAVAR_LITERAL); +ELLIPSIS_LITERAL_NLSEMI : '"' '...' '"' -> type(ELLIPSIS_LITERAL); @@ -67,7 +69,7 @@ @@ -39,10 +39,52 @@ superClass = GoParserBase; } - + +@parser::members { + // Semgrep: enable the `operand` (composite-literal) alternative for a qualified + // type literal `pkg.T{...}`. Without imports the base predicate routes `pkg.T` @@ -291,14 +293,17 @@ ; conversion -@@ -424,6 +478,7 @@ +@@ -424,6 +478,9 @@ operand - : literal +- : literal ++ : METAVAR_STAR_IDENT ++ | literal | operandName typeArgs? ++ | L_PAREN METAVAR_STAR_IDENT COLON type_ R_PAREN + | L_PAREN METAVAR_IDENT COLON type_ R_PAREN | L_PAREN expression R_PAREN ; - + @@ -451,11 +506,17 @@ operandName diff --git a/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/rule.yaml new file mode 100644 index 000000000..db459c55e --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-01-sink-field + languages: [go] + severity: WARNING + message: Tainted struct field reaches a starred whole-object sink + mode: taint + pattern-sources: + - pattern: star_01_sink_field.Source(...) + pattern-sinks: + - patterns: + - pattern: star_01_sink_field.Sink_Box($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/sample.go new file mode 100644 index 000000000..7eeec9174 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/sample.go @@ -0,0 +1,26 @@ +package util + +// Box carries a single string field; the star operator lets a whole-object sink +// observe taint that lives on a nested field rather than the object's base value. +type Box struct { + Value string +} + +func Source() string { return "tainted" } + +func Sink_Box(b Box) { _ = b } + +// Positive_tainted_field: a source-tainted value is written into b.Value (a nested +// field). The starred sink Sink_Box($*Y) matches the field taint on the whole object. +func Positive_tainted_field() { + var b Box + b.Value = Source() + Sink_Box(b) +} + +// Negative_clean_object: the field is never tainted, so the starred sink stays silent. +func Negative_clean_object() { + var b Box + b.Value = "safe" + Sink_Box(b) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/rule.yaml new file mode 100644 index 000000000..1909a1491 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-02-source-field + languages: [go] + severity: WARNING + message: A starred whole-object source taints every field; a field read reaches the sink + mode: taint + pattern-sources: + - pattern: $*X = star_02_source_field.Source() + pattern-sinks: + - pattern: star_02_source_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/sample.go new file mode 100644 index 000000000..d09ce770c --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/sample.go @@ -0,0 +1,25 @@ +package util + +// Data is the whole object tainted by the starred source; every nested field +// inherits the taint, so a later field read is tainted too. +type Data struct { + Field string +} + +func Source() Data { return Data{Field: "tainted"} } + +func Sink(s string) { _ = s } + +// Positive_field_read: the starred source ($*X = Source()) taints the whole object +// AND all its fields; the field read d.Field then reaches the plain sink. +func Positive_field_read() { + d := Source() + Sink(d.Field) +} + +// Negative_untainted: the object is built from a constant, so no field is tainted. +func Negative_untainted() { + var d Data + d.Field = "safe" + Sink(d.Field) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/rule.yaml new file mode 100644 index 000000000..e74839e91 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-03-sanitizer-field + languages: [go] + severity: WARNING + message: Tainted struct field reaches the sink unless a starred sanitizer clears the whole object + mode: taint + pattern-sources: + - pattern: star_03_sanitizer_field.Source(...) + pattern-sanitizers: + - patterns: + - pattern: star_03_sanitizer_field.Clean($*C) + - focus-metavariable: $C + pattern-sinks: + - pattern: star_03_sanitizer_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/sample.go new file mode 100644 index 000000000..ef6ce4154 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/sample.go @@ -0,0 +1,30 @@ +package util + +// Box carries the tainted field. The starred sanitizer Clean($*C) must clear the +// taint on the whole object INCLUDING the nested field, so a later field read is clean. +type Box struct { + Value string +} + +func Source() string { return "tainted" } + +// Clean is the $*C sanitizer: it clears the argument object and all of its fields. +func Clean(b Box) Box { return b } + +func Sink(s string) { _ = s } + +// Positive_unsanitized: field taint reaches the sink with no sanitizer in between. +func Positive_unsanitized() { + var b Box + b.Value = Source() + Sink(b.Value) +} + +// Negative_sanitized: the starred sanitizer sits between source and sink; if $*C truly +// clears the concrete nested-field taint, the field read must be clean and nothing reports. +func Negative_sanitized() { + var b Box + b.Value = Source() + cleaned := Clean(b) + Sink(cleaned.Value) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/rule.yaml new file mode 100644 index 000000000..d9ce0b3c8 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-04-deep-sink-field + languages: [go] + severity: WARNING + message: Taint hidden 5 fields deep reaches a starred whole-object sink + mode: taint + pattern-sources: + - pattern: star_04_deep_sink_field.Source(...) + pattern-sinks: + - patterns: + - pattern: star_04_deep_sink_field.Sink_L0($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/sample.go new file mode 100644 index 000000000..06e8bb916 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/sample.go @@ -0,0 +1,50 @@ +package util + +// L0..L4 nest a string field 5 levels deep. The starred whole-object sink must observe +// taint that lives on a nested field. +type L0 struct { + V0 string + F *L1 +} +type L1 struct { + V1 string + F *L2 +} +type L2 struct { + V2 string + F *L3 +} +type L3 struct { + V3 string + F *L4 +} +type L4 struct{ V string } + +func Source() string { return "tainted" } + +func Sink_L0(b L0) { _ = b } + +func build() L0 { + return L0{F: &L1{F: &L2{F: &L3{F: &L4{}}}}} +} + +// Positive_depth1: taint at field depth 1; the starred sink observes it. +func Positive_depth1() { + o := build() + o.V0 = Source() + Sink_L0(o) +} + +// Positive_depth5: taint hidden 5 fields deep; the starred sink must still match. +func Positive_depth5() { + o := build() + o.F.F.F.F.V = Source() + Sink_L0(o) +} + +// Negative_clean_object: no field ever tainted, so the starred sink stays silent. +func Negative_clean_object() { + o := build() + o.F.F.F.F.V = "safe" + Sink_L0(o) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/rule.yaml new file mode 100644 index 000000000..d7a88202a --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-05-deep-source-field + languages: [go] + severity: WARNING + message: A starred whole-object source taints every nested field; a deep field read reaches the sink + mode: taint + pattern-sources: + - pattern: $*X = star_05_deep_source_field.Source() + pattern-sinks: + - pattern: star_05_deep_source_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/sample.go new file mode 100644 index 000000000..26ebfc9ec --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/sample.go @@ -0,0 +1,27 @@ +package util + +// The starred whole-object source taints every nested field at every depth; a 5-level +// field read must therefore be tainted too. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Sink(s string) { _ = s } + +// Positive_deep_field_read: the starred source ($*X = Source()) taints the whole object AND +// all nested fields; the depth-5 field read o.F.F.F.F.V then reaches the plain sink. +func Positive_deep_field_read() { + o := Source() + Sink(o.F.F.F.F.V) +} + +// Negative_untainted: the object is built from constants, so no field is tainted. +func Negative_untainted() { + var o L0 + o.F.F.F.F.V = "safe" + Sink(o.F.F.F.F.V) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/rule.yaml new file mode 100644 index 000000000..91ddd7bd4 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-06-deep-sanitizer-field + languages: [go] + severity: WARNING + message: Deep struct-field taint reaches the sink unless a starred sanitizer clears the whole object + mode: taint + pattern-sources: + - pattern: star_06_deep_sanitizer_field.Source(...) + pattern-sanitizers: + - patterns: + - pattern: star_06_deep_sanitizer_field.Clean($*C) + - focus-metavariable: $C + pattern-sinks: + - pattern: star_06_deep_sanitizer_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/sample.go new file mode 100644 index 000000000..9873f03cc --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/sample.go @@ -0,0 +1,35 @@ +package util + +// L0..L3 nest a string field 4 levels deep. The starred sanitizer Clean($*C) must clear the +// taint on the whole object INCLUDING the nested field, so a later deep field read is clean. +type L0 struct{ F *L1 } +type L1 struct{ F *L2 } +type L2 struct{ F *L3 } +type L3 struct{ V string } + +func Source() string { return "tainted" } + +// Clean is the $*C sanitizer: it clears the argument object and all of its nested fields. +func Clean(b L0) L0 { return b } + +func Sink(s string) { _ = s } + +func build() L0 { + return L0{F: &L1{F: &L2{F: &L3{}}}} +} + +// Positive_unsanitized: deep field taint reaches the sink with no sanitizer in between. +func Positive_unsanitized() { + o := build() + o.F.F.F.V = Source() + Sink(o.F.F.F.V) +} + +// Negative_sanitized: the starred sanitizer sits between source and sink; if $*C truly clears +// the concrete deep-field taint, the field read must be clean and nothing reports. +func Negative_sanitized() { + o := build() + o.F.F.F.V = Source() + cleaned := Clean(o) + Sink(cleaned.F.F.F.V) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/rule.yaml new file mode 100644 index 000000000..0468b6730 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-07-interproc-chain + languages: [go] + severity: WARNING + message: A starred whole-object source survives a 5+ hop hide/expose interprocedural chain + mode: taint + pattern-sources: + - pattern: $*X = star_07_interproc_chain.Source() + pattern-sinks: + - pattern: star_07_interproc_chain.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/sample.go new file mode 100644 index 000000000..64616a110 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/sample.go @@ -0,0 +1,52 @@ +package util + +// Box carries a single string field. A starred whole-object source is threaded through a 5+ +// hop interprocedural chain that alternately hides taint inside the object and exposes it. +type Box struct{ V string } + +func Source() Box { return Box{} } + +func Sink(s string) { _ = s } + +// step1..step5: 5 interprocedural hops. Alternation: +// step1 pass object -> step2 EXPOSE field to scalar -> step3 HIDE scalar in a new Box +// -> step4 pass object -> step5 EXPOSE the field again, reaching the sink. +func step1(b Box) Box { return b } +func step2(b Box) string { return b.V } +func step3(s string) Box { return Box{V: s} } +func step4(b Box) Box { return b } +func step5(b Box) string { return b.V } + +// Positive_alternating_chain: taint survives 5 hops of hide/expose alternation from a starred +// source ($*X = Source()). +func Positive_alternating_chain() { + b := Source() + b1 := step1(b) + s2 := step2(b1) + b3 := step3(s2) + b4 := step4(b3) + s5 := step5(b4) + Sink(s5) +} + +// Positive_passthrough_chain: simplest 5-hop pass-through, field exposed only at the end. +func Positive_passthrough_chain() { + b := Source() + b1 := step1(b) + b2 := step1(b1) + b3 := step1(b2) + b4 := step4(b3) + s := step5(b4) + Sink(s) +} + +// Negative_clean_chain: a fresh untainted Box threaded through the same chain. +func Negative_clean_chain() { + b := Box{V: "safe"} + b1 := step1(b) + s2 := step2(b1) + b3 := step3(s2) + b4 := step4(b3) + s5 := step5(b4) + Sink(s5) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/rule.yaml new file mode 100644 index 000000000..431aa6f74 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-08-source-and-sink + languages: [go] + severity: WARNING + message: A whole-object source and a whole-object sink compose across a nested extraction + mode: taint + pattern-sources: + - pattern: $*X = star_08_source_and_sink.Source() + pattern-sinks: + - patterns: + - pattern: star_08_source_and_sink.Sink_Inner($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/sample.go new file mode 100644 index 000000000..e4fd5582f --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/sample.go @@ -0,0 +1,26 @@ +package util + +// Both ends starred: a whole-object source ($*X = Source()) taints every nested field, and a +// whole-object sink (Sink_Inner($*Y)) observes a nested sub-object pulled out in between. +type Outer struct{ F Mid } +type Mid struct{ F Inner } +type Inner struct{ V string } + +func Source() Outer { return Outer{} } + +func Sink_Inner(i Inner) { _ = i } + +// Positive_nested_object_to_star_sink: the whole-object source taint reaches a nested +// sub-object handed to the starred sink. +func Positive_nested_object_to_star_sink() { + o := Source() + inner := o.F.F + Sink_Inner(inner) +} + +// Negative_clean_nested: locally-built object, nothing tainted. +func Negative_clean_nested() { + var o Outer + o.F.F.V = "safe" + Sink_Inner(o.F.F) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/rule.yaml new file mode 100644 index 000000000..32866b91d --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-09-matrix-source + languages: [go] + severity: WARNING + message: Starred source 5 calls deep survives per-hop field unwrapping into a 5-deep sink chain + mode: taint + pattern-sources: + - pattern: $*X = star_09_matrix_source.Source() + pattern-sinks: + - pattern: star_09_matrix_source.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/sample.go new file mode 100644 index 000000000..8d2ac9f29 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/sample.go @@ -0,0 +1,53 @@ +package util + +// Starred SOURCE, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixSource). The source statement `$*X = Source()` sits FIVE calls deep +// (src1..src5); the tainted whole object then climbs back up and is unwrapped ONE field +// level per hop across five more calls (u1..u5, L0->..->string), and the scalar finally +// travels five calls down a sink chain (k1..k5) to a plain sink. The 5-level field taint +// is carried by the $* source's abstract any-field mark. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Sink(s string) { _ = s } + +// Source five calls deep: the starred source statement is inside src1. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five hops, each unwrapping exactly one field level: interproc depth x field depth. +func u1(o L0) L1 { return o.F } +func u2(o L1) L2 { return o.F } +func u3(o L2) L3 { return o.F } +func u4(o L3) L4 { return o.F } +func u5(o L4) string { return o.V } + +// Sink five calls deep. +func k1(s string) { k2(s) } +func k2(s string) { k3(s) } +func k3(s string) { k4(s) } +func k4(s string) { k5(s) } +func k5(s string) { Sink(s) } // Sink() called HERE, depth 5 + +// Positive_deep_chain: deep source -> 5x1-field unwrap hops -> deep sink. +func Positive_deep_chain() { + o := src5() + s := u5(u4(u3(u2(u1(o))))) + k1(s) +} + +// Negative_clean_chain: an untainted object through the identical chains. +func Negative_clean_chain() { + var o L0 + o.F.F.F.F.V = "safe" + s := u5(u4(u3(u2(u1(o))))) + k1(s) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/rule.yaml new file mode 100644 index 000000000..0831cf79a --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-10-matrix-sink + languages: [go] + severity: WARNING + message: Whole-object taint wrapped 5 field levels deep reaches a starred sink 5 calls deep + mode: taint + pattern-sources: + - pattern: $*X = star_10_matrix_sink.Source() + pattern-sinks: + - patterns: + - pattern: star_10_matrix_sink.Sink_L0($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/sample.go new file mode 100644 index 000000000..aa7952eef --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/sample.go @@ -0,0 +1,53 @@ +package util + +// Starred SINK, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixSink). A starred source taints the INNERMOST object (L4) five calls deep; +// five hops then each WRAP it one level deeper (L4->L3->..->L0), and the outermost object +// travels five calls down a sink chain to `Sink_L0($*Y)` — the starred sink must observe +// the whole-object taint buried five field levels down the wrapped object. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L4 { return L4{} } + +func Sink_L0(o L0) { _ = o } + +// Source five calls deep: the starred source statement is inside src1. +func src5() L4 { return src4() } +func src4() L4 { return src3() } +func src3() L4 { return src2() } +func src2() L4 { return src1() } +func src1() L4 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five hops, each WRAPPING one field level (hide direction). +func w1(o L4) L3 { var n L3; n.F = o; return n } +func w2(o L3) L2 { var n L2; n.F = o; return n } +func w3(o L2) L1 { var n L1; n.F = o; return n } +func w4(o L1) L0 { var n L0; n.F = o; return n } +func w5(o L0) L0 { return o } + +// Sink five calls deep. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { Sink_L0(o) } // Sink_L0($*Y) matches HERE, depth 5 + +// Positive_wrapped_deep: the tainted L4 is wrapped five levels deep; the starred sink +// observes it. +func Positive_wrapped_deep() { + t := src5() + o := w5(w4(w3(w2(w1(t))))) + k1(o) +} + +// Negative_clean_wrapped: an untainted L4 wrapped and threaded through the identical chains. +func Negative_clean_wrapped() { + var t L4 + t.V = "safe" + o := w5(w4(w3(w2(w1(t))))) + k1(o) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/rule.yaml new file mode 100644 index 000000000..24fb53d4b --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-11-matrix-propagator + languages: [go] + severity: WARNING + message: A doubly-starred propagator moves whole-object taint into a fresh object mid-chain + mode: taint + pattern-sources: + - pattern: $*X = star_11_matrix_propagator.Source() + pattern-propagators: + - pattern: $*T = star_11_matrix_propagator.Pass($*F) + from: $F + to: $T + pattern-sinks: + - pattern: star_11_matrix_propagator.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/sample.go new file mode 100644 index 000000000..cadfad9ed --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/sample.go @@ -0,0 +1,74 @@ +package util + +// Starred PROPAGATOR — BOTH occurrences starred (`$*T = Pass($*F)`) — at 5+ interprocedural +// depth x 5+ field depth (Go port of StarMatrixPropagator). A starred source five calls deep +// taints a whole L0; the object travels five pass-hops to the propagator call, whose starred +// FROM observes the any-field taint of the whole argument and whose starred TO assigns +// whole-object taint to the fresh M0 result. The M0 is then unwrapped ONE field level per hop +// across five calls (M0->..->string) — only possible if the TO really carries any-field taint +// — and the scalar travels five calls down a sink chain to a plain sink. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +type M0 struct{ F M1 } +type M1 struct{ F M2 } +type M2 struct{ F M3 } +type M3 struct{ F M4 } +type M4 struct{ V string } + +func Source() L0 { return L0{} } + +func Pass(o L0) M0 { _ = o; return M0{} } + +func Sink(s string) { _ = s } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops before the propagator. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Five hops, each unwrapping one field level of the PROPAGATED object: taint reaches the +// scalar only if the starred TO assigned any-field taint to the M0. +func u1(o M0) M1 { return o.F } +func u2(o M1) M2 { return o.F } +func u3(o M2) M3 { return o.F } +func u4(o M3) M4 { return o.F } +func u5(o M4) string { return o.V } + +// Sink five calls deep. +func k1(s string) { k2(s) } +func k2(s string) { k3(s) } +func k3(s string) { k4(s) } +func k4(s string) { k5(s) } +func k5(s string) { Sink(s) } // Sink() called HERE, depth 5 + +// Positive_propagated_deep: deep source -> 5 hops -> starred propagator -> per-hop unwrap +// -> deep sink. +func Positive_propagated_deep() { + o := src5() + o5 := p5(p4(p3(p2(p1(o))))) + t := Pass(o5) // $*T = Pass($*F): whole object in, whole object out + s := u5(u4(u3(u2(u1(t))))) + k1(s) +} + +// Negative_clean_propagated: an untainted object through the identical propagator and chains. +func Negative_clean_propagated() { + var o L0 + o5 := p5(p4(p3(p2(p1(o))))) + t := Pass(o5) + s := u5(u4(u3(u2(u1(t))))) + k1(s) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/rule.yaml new file mode 100644 index 000000000..1dbb1b054 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-12-matrix-sanitizer + languages: [go] + severity: WARNING + message: Deep whole-object taint reaches the sink unless a starred sanitizer inside a wrapper clears it + mode: taint + pattern-sources: + - pattern: $*X = star_12_matrix_sanitizer.Source() + pattern-sanitizers: + - patterns: + - pattern: star_12_matrix_sanitizer.Clean($*C) + - focus-metavariable: $C + pattern-sinks: + - pattern: star_12_matrix_sanitizer.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/sample.go new file mode 100644 index 000000000..2b427622b --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/sample.go @@ -0,0 +1,61 @@ +package util + +// Starred SANITIZER, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixSanitizer). A starred source five calls deep taints a whole L0. On the sanitized +// path the object goes through `Sanitize()` — a HELPER whose body calls the starred-clean +// `Clean()` (the wrapper shape behind the OWASP escapeHtml FPs, i.e. the deep-mark-exclusion +// fix's sample-level regression test). Afterwards five hops unwrap one field level each and +// the scalar travels five calls down to the sink; the whole-object clean must have removed +// the any-field taint at every depth. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Clean(o L0) L0 { return o } + +func Sink(s string) { _ = s } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// The starred clean sits INSIDE a wrapper: its whole-object effect must survive the +// wrapper's interprocedural summary (deep mark exclusions). +func Sanitize(o L0) L0 { return Clean(o) } + +// Five hops, each unwrapping exactly one field level. +func u1(o L0) L1 { return o.F } +func u2(o L1) L2 { return o.F } +func u3(o L2) L3 { return o.F } +func u4(o L3) L4 { return o.F } +func u5(o L4) string { return o.V } + +// Sink five calls deep. +func k1(s string) { k2(s) } +func k2(s string) { k3(s) } +func k3(s string) { k4(s) } +func k4(s string) { k5(s) } +func k5(s string) { Sink(s) } // Sink() called HERE, depth 5 + +// Positive_unsanitized_deep: the unsanitized path flags. +func Positive_unsanitized_deep() { + o := src5() + s := u5(u4(u3(u2(u1(o))))) + k1(s) +} + +// Negative_sanitized_deep: the wrapped whole-object clean clears the taint at every field +// depth. +func Negative_sanitized_deep() { + o := src5() + c := Sanitize(o) + s := u5(u4(u3(u2(u1(c))))) + k1(s) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/rule.yaml new file mode 100644 index 000000000..3b95b9938 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/rule.yaml @@ -0,0 +1,13 @@ +rules: + - id: star-13-matrix-pattern-not + languages: [go] + severity: WARNING + message: Starred sink with a starred pattern-not exclusion — flagged mode fires, safe mode is excluded + mode: taint + pattern-sources: + - pattern: $*X = star_13_matrix_pattern_not.Source() + pattern-sinks: + - patterns: + - pattern: star_13_matrix_pattern_not.Emit($*Y, $MODE) + - pattern-not: star_13_matrix_pattern_not.Emit($*Y, "safe") + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/sample.go new file mode 100644 index 000000000..399db76e5 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/sample.go @@ -0,0 +1,57 @@ +package util + +// Starred PATTERN-NOT sink, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixPatternNot). The sink is `Emit($*Y, $MODE)` with +// `pattern-not: Emit($*Y, "safe")` — the starred metavar occurrence appears in BOTH the +// pattern and the pattern-not (the constraint solver keeps $Y and $*Y distinct, so the forms +// must agree). A starred source five calls deep taints a whole L0; the object travels five +// hops and is emitted five calls deep — flagged in "html" mode, excluded by the pattern-not +// in "safe" mode. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Emit(o L0, mode string) { _, _ = o, mode } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Two sink chains five calls deep: one emits in a flagged mode, one in the excluded mode. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { Emit(o, "html") } // matches the sink, depth 5 + +func j1(o L0) { j2(o) } +func j2(o L0) { j3(o) } +func j3(o L0) { j4(o) } +func j4(o L0) { j5(o) } +func j5(o L0) { Emit(o, "safe") } // excluded by pattern-not, depth 5 + +// Positive_emit_html: tainted object emitted in a non-excluded mode. +func Positive_emit_html() { + o := src5() + k1(p5(p4(p3(p2(p1(o)))))) +} + +// Negative_emit_safe: same tainted object, but the emit call matches the pattern-not. +func Negative_emit_safe() { + o := src5() + j1(p5(p4(p3(p2(p1(o)))))) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/rule.yaml new file mode 100644 index 000000000..5ac635e22 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/rule.yaml @@ -0,0 +1,15 @@ +rules: + - id: star-14-matrix-pattern-inside + languages: [go] + severity: WARNING + message: Starred sink gated by a pattern-inside-introduced receiver + mode: taint + pattern-sources: + - pattern: $*X = star_14_matrix_pattern_inside.Source() + pattern-sinks: + - patterns: + - pattern-inside: | + $R = star_14_matrix_pattern_inside.OpenSink() + ... + - pattern: $R.Consume($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/sample.go new file mode 100644 index 000000000..653d217c9 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/sample.go @@ -0,0 +1,70 @@ +package util + +// Starred sink gated by PATTERN-INSIDE, 5+ interprocedural depth x 5+ field depth combined +// (Go port of StarMatrixPatternInside). The sink `$R.Consume($*Y)` only counts when the +// receiver comes from `OpenSink()` in the same function (pattern-inside). A starred source +// five calls deep taints a whole L0; the object travels five hops; the Consume call sits five +// calls deep. The gated function uses OpenSink() (flagged); the ungated one obtains its +// receiver elsewhere (not a sink at all). +type Out struct{} + +func (r Out) Consume(o L0) { _ = o } + +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func OpenSink() Out { return Out{} } + +func PlainOut() Out { return Out{} } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Sink chain five calls deep, ending in the pattern-inside-gated Consume. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { + r := OpenSink() // pattern-inside context + r.Consume(o) // starred sink matches HERE, depth 5 +} + +// Same-depth chain whose Consume receiver does NOT come from OpenSink(). +func j1(o L0) { j2(o) } +func j2(o L0) { j3(o) } +func j3(o L0) { j4(o) } +func j4(o L0) { j5(o) } +func j5(o L0) { + r := PlainOut() // no pattern-inside context + r.Consume(o) +} + +// Positive_gated_consume: tainted object consumed inside the gated context. +func Positive_gated_consume() { + o := src5() + k1(p5(p4(p3(p2(p1(o)))))) +} + +// Negative_ungated_consume: same tainted object, but the Consume call lacks the +// pattern-inside context. +func Negative_ungated_consume() { + o := src5() + j1(p5(p4(p3(p2(p1(o)))))) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/rule.yaml new file mode 100644 index 000000000..620a4a9c8 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/rule.yaml @@ -0,0 +1,18 @@ +rules: + - id: star-15-matrix-pattern-not-inside + languages: [go] + severity: WARNING + message: Starred sink suppressed by a pattern-not-inside guard wired through the pattern-inside + mode: taint + pattern-sources: + - pattern: $*X = star_15_matrix_pattern_not_inside.Source() + pattern-sinks: + - patterns: + - pattern-inside: | + $G = star_15_matrix_pattern_not_inside.NewChecker() + ... + - pattern: star_15_matrix_pattern_not_inside.Use($*Y) + - pattern-not-inside: | + $G.Check($*Y) + ... + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/sample.go new file mode 100644 index 000000000..fb96a301b --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/sample.go @@ -0,0 +1,73 @@ +package util + +// Starred sink guarded by PATTERN-NOT-INSIDE, 5+ interprocedural depth x 5+ field depth +// combined (Go port of StarMatrixPatternNotInside). The sink `Use($*Y)` sits in a +// `pattern-inside` context that INTRODUCES the guard receiver (`$G = NewChecker(); ...`), +// and `pattern-not-inside: $G.Check($*Y); ...` suppresses it — every not-inside metavar must +// be introduced and wired by the pattern-inside/sink patterns (a not-inside with unbound +// metavars is dropped during automata-to-taint-rule conversion). A starred source five calls +// deep taints a whole L0; the object travels five hops; the Use call sits five calls deep — +// flagged in the unguarded function, suppressed in the guarded one. +type Checker struct{} + +func (c Checker) Check(o L0) { _ = o } + +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func NewChecker() Checker { return Checker{} } + +func Use(o L0) { _ = o } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Unguarded sink chain five calls deep. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { + g := NewChecker() // pattern-inside context (binds $G), no Check() -> flagged + _ = g + Use(o) // starred sink matches HERE, depth 5 +} + +// Guarded sink chain five calls deep: Check() precedes the Use in the same function. +func j1(o L0) { j2(o) } +func j2(o L0) { j3(o) } +func j3(o L0) { j4(o) } +func j4(o L0) { j5(o) } +func j5(o L0) { + g := NewChecker() // pattern-inside context (binds $G) + g.Check(o) // pattern-not-inside: $G.Check($*Y) precedes -> suppressed + Use(o) +} + +// Positive_unguarded_use: tainted object used without the guard. +func Positive_unguarded_use() { + o := src5() + k1(p5(p4(p3(p2(p1(o)))))) +} + +// Negative_guarded_use: same tainted object, but the Use is preceded by Check(). +func Negative_guarded_use() { + o := src5() + j1(p5(p4(p3(p2(p1(o)))))) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml index b5ce924cb..887887221 100644 --- a/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: $R.FormValue($K) pattern-sinks: - - pattern: $W.Write($B) + - patterns: + - pattern: $W.Write($*B) + - focus-metavariable: $B diff --git a/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml index 79540c97f..ede34dcc2 100644 --- a/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml @@ -8,5 +8,5 @@ rules: - pattern: os.Getenv($K) pattern-sinks: - patterns: - - pattern: $T.Execute($W, $D) + - pattern: $T.Execute($W, $*D) - focus-metavariable: $D diff --git a/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml b/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml index 04a3c1bb7..e705e5fd8 100644 --- a/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml @@ -9,7 +9,15 @@ rules: - pattern: os.Getenv($K) pattern-sinks: - pattern-either: - - pattern: $C.CombinedOutput() - - pattern: $C.Run() - - pattern: $C.Output() - - pattern: $C.Start() + - patterns: + - pattern: $*C.CombinedOutput() + - focus-metavariable: $C + - patterns: + - pattern: $*C.Run() + - focus-metavariable: $C + - patterns: + - pattern: $*C.Output() + - focus-metavariable: $C + - patterns: + - pattern: $*C.Start() + - focus-metavariable: $C diff --git a/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml b/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml index 4bd5388b7..ba427405e 100644 --- a/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml @@ -2,16 +2,16 @@ rules: - id: cmd-typed-receiver-sink languages: [go] severity: ERROR - message: Tainted user input reaches OS command execution via a typed *exec.Cmd receiver + message: Tainted user input reaches OS command execution via an *exec.Cmd receiver mode: taint pattern-sources: - pattern-either: - pattern: os.Getenv($K) pattern-sinks: - pattern-either: - - pattern: | - import "os/exec" - ($C : *exec.Cmd).Run() - - pattern: | - import "os/exec" - ($C : *exec.Cmd).CombinedOutput() + - patterns: + - pattern: "($*C : *exec.Cmd).Run()" + - focus-metavariable: $C + - patterns: + - pattern: "($*C : *exec.Cmd).CombinedOutput()" + - focus-metavariable: $C diff --git a/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml b/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml index 31b446ce8..7d5c29f3b 100644 --- a/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: "MapValueToReceiver.Source(...)" pattern-sinks: - - pattern: "($C : *MapValueToReceiver.Controller).Serve()" + - patterns: + - pattern: "$*C.Serve()" + - focus-metavariable: $C diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt index d3e63c5fb..095a96718 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt @@ -28,7 +28,7 @@ data class MetavarName(val name: String) : Name // 5.1 Pattern atoms // ---------------------------------------------------------------------------- -data class Metavar(val name: String) : SemgrepGoPattern { +data class Metavar(val name: String, val star: Boolean = false) : SemgrepGoPattern { override val children: List get() = emptyList() } @@ -44,7 +44,7 @@ data class DeepExpr(val nested: SemgrepGoPattern) : SemgrepGoPattern { override val children: List get() = listOf(nested) } -data class TypedMetavar(val name: String, val type: TypeName) : SemgrepGoPattern { +data class TypedMetavar(val name: String, val type: TypeName, val star: Boolean = false) : SemgrepGoPattern { override val children: List get() = emptyList() } diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt index f454a19b3..9e7096ea3 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt @@ -662,20 +662,36 @@ private class SemgrepGoPatternParserVisitor : GoParserBaseVisitor `$X`: drop the `*` that follows the leading `$`, yielding the plain metavar name. + private fun String.stripStar(): String = "$" + substring(2) + private fun parseOperandName(ctx: GoParser.OperandNameContext): SemgrepGoPattern { ctx.qualifiedIdent()?.let { val (pkg, sel) = parseQualifiedIdentParts(it) diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt index 3a586d92f..10736069f 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt @@ -353,12 +353,17 @@ class GoPatternToActionListConverter : ActionListBuilder { is MetavarName -> Triple(emptyList(), IsMetavar(MetavarAtom.create(n.name)), null) } - is Metavar -> Triple(emptyList(), IsMetavar(MetavarAtom.create(recv.name)), null) + is Metavar -> Triple(emptyList(), IsMetavar(MetavarAtom.create(recv.name), star = recv.star), null) is TypedMetavar -> { val t = transformType(recv.type) Triple( emptyList(), - ParamCondition.And(listOf(IsMetavar(MetavarAtom.create(recv.name)), ParamCondition.TypeIs(t))), + ParamCondition.And( + listOf( + IsMetavar(MetavarAtom.create(recv.name), star = recv.star), + ParamCondition.TypeIs(t), + ), + ), null, ) } @@ -458,10 +463,10 @@ class GoPatternToActionListConverter : ActionListBuilder { is MetavarName -> ParamCondition.StringValueMetaVar(MetavarAtom.create(c.name)) } is StringEllipsis -> ParamCondition.AnyStringLiteral - is Metavar -> IsMetavar(MetavarAtom.create(pattern.name)) + is Metavar -> IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star) is TypedMetavar -> ParamCondition.And( listOf( - IsMetavar(MetavarAtom.create(pattern.name)), + IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star), ParamCondition.TypeIs(transformType(pattern.type)), ), ) @@ -482,7 +487,7 @@ class GoPatternToActionListConverter : ActionListBuilder { if (names.size == 1) { val name = names.first() if (name != null) { - conditions += IsMetavar(MetavarAtom.create(name)) + conditions += IsMetavar(MetavarAtom.create(name.name), star = name.star) } return transformAssignmentValue(conditions, value) @@ -498,22 +503,27 @@ class GoPatternToActionListConverter : ActionListBuilder { } val assignedName = names[assignedNameIdx]!! - conditions += IsMetavar(MetavarAtom.create(assignedName)) + conditions += IsMetavar(MetavarAtom.create(assignedName.name), star = assignedName.star) conditions += createFieldModifier(prevModifier = null, "tuple$$assignedNameIdx") return transformAssignmentValue(conditions, value) } + // Name + star of an assignment target. A bare `Metavar` (`$*X`) or a typed metavar (`($*X : T)`) + // can be starred; other target shapes carry star = false. Threading star lets `$*X = src()` (or + // its typed form) taint every nested field. + private data class AssignmentTarget(val name: String, val star: Boolean) + private fun SemgrepGoPattern.assignmentTargetName( conditions: MutableList - ): String? = when { - this is Metavar -> name + ): AssignmentTarget? = when { + this is Metavar -> AssignmentTarget(name, star) this is TypedMetavar -> { conditions += ParamCondition.TypeIs(transformType(type)) - name + AssignmentTarget(name, star = star) } - this is Identifier && name is MetavarName -> name.name + this is Identifier && name is MetavarName -> AssignmentTarget(name.name, star = false) this is Identifier && name is ConcreteName && name.name == "_" -> null else -> transformationFailed("Assignment_target_not_metavar") diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt index 7fa451789..4f180cd4f 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt @@ -51,6 +51,9 @@ data object GoTaintStrategy : data object GoMarkConditionBuilder : MarkConditionBuilder { override fun checkTaintMark(mark: Mark.GeneratedMark, pos: PositionBaseWithModifiers): GoSerializedCondition = + mark.mkGoContainsMark(pos) + + override fun checkTaintMarkOnAnyField(mark: Mark.GeneratedMark, pos: PositionBaseWithModifiers): GoSerializedCondition = mark.mkGoContainsMarkOnAnyAccessor(pos) override fun negate(cond: GoSerializedCondition) = GoSerializedCondition.not(cond) diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt index 3ecbbc358..45e7c7f3b 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt @@ -249,8 +249,19 @@ private fun GoTaintRuleGenerationCtx.buildGoStateAssignActions( val requiredVariables = stateAfter.register.assignedVars.keys val result = requiredVariables.flatMapTo(mutableListOf()) { varName -> val varPosition = edgeCondition.accessedVarPosition[varName] ?: return@flatMapTo emptyList() - varPosition.positions.flatMap { stateAssignMark(varPosition.varName, stateAfter, it) } + varPosition.positions.flatMap { sp -> + val assigns = stateAssignMark(varPosition.varName, stateAfter, sp.position) + if (!sp.star) return@flatMap assigns + // Starred source ($*X): also taint every nested field. Mirror each emitted Direct with an + // any-accessor assign on the SAME mark/position (the engine-supported whole-object taint). + assigns + assigns.map { GoSerializedAssignAction.AnyAccessor(it.kind, it.rawPosition()) } + } + } + + if (stateAfter in globalStateAssignStates) { + result += globalStateMarkName(stateAfter).mkGoAssignMark(goStateVarPosition) } + return result } @@ -260,12 +271,27 @@ private fun GoTaintRuleGenerationCtx.buildGoStateCleanActions( edgeCondition: GoEvaluatedEdgeCondition ): List { val result = edgeCondition.accessedVarPosition.values.flatMapTo(mutableListOf()) { varPosition -> - varPosition.positions.flatMap { stateCleanMark(varPosition.varName, stateAfter, stateBefore, it) } + varPosition.positions.flatMap { sp -> + val cleans = stateCleanMark(varPosition.varName, stateAfter, stateBefore, sp.position) + if (!sp.star) return@flatMap cleans + // Starred sanitizer ($*C): clean the base value AND every nested field. The any-accessor + // clean removes marks stored under an ANY accessor but does NOT reach a concrete base + // mark, so the base Direct clean is REQUIRED too -- emit BOTH (mirrors the source side). + cleans + cleans.map { GoSerializedCleanAction.AnyAccessor(it.taintKind, it.pos) } + } } result += stateCleanMark(varName = null, stateAfter, stateBefore, position = null) + + if (stateBefore in globalStateAssignStates) { + result += globalStateMarkName(stateBefore).mkGoCleanMark(goStateVarPosition) + } + return result } +private val GoTaintRuleGenerationCtx.goStateVarPosition: PositionBaseWithModifiers + get() = PositionBase.ClassStatic(prefix.artificialState("pos").taintMarkStr()).baseGo() + private fun GoEvaluatedEdgeCondition.addGoStateCheck( ctx: GoTaintRuleGenerationCtx, checkGlobalState: Boolean, @@ -273,13 +299,13 @@ private fun GoEvaluatedEdgeCondition.addGoStateCheck( ): GoEvaluatedEdgeCondition { val stateChecks = mutableListOf() if (checkGlobalState) { - stateChecks += ctx.globalStateMarkName(stateOfEdge).mkGoContainsMark( - PositionBase.ClassStatic(ctx.prefix.artificialState("pos").taintMarkStr()).baseGo() - ) + stateChecks += ctx.globalStateMarkName(stateOfEdge).mkGoContainsMark(ctx.goStateVarPosition) } else { for (metaVar in stateOfEdge.register.assignedVars.keys) { - for (pos in accessedVarPosition[metaVar]?.positions.orEmpty()) { - stateChecks += ctx.containsStateMark(metaVar, stateOfEdge, pos) + for (sp in accessedVarPosition[metaVar]?.positions.orEmpty()) { + stateChecks += ctx.containsStateMark(metaVar, stateOfEdge, sp.position) + // Starred sink ($*Y): also match when any nested field carries the mark. + if (sp.star) stateChecks += ctx.containsStateMarkOnAnyField(metaVar, stateOfEdge, sp.position) } } } @@ -609,7 +635,7 @@ private fun findGoMetaVarPositionUtil( val varPosition = varPositions.getOrPut(condition.metavar) { GoRegisterVarPosition(condition.metavar, hashSetOf()) } - varPosition.positions.add(position) + varPosition.positions.add(GoStarredPosition(position, condition.star)) } private fun evaluateGoParamCondition( @@ -627,7 +653,12 @@ private fun evaluateGoParamCondition( // todo: semantic metavar constraint semgrepRuleTrace.error(IgnoredMetavarConstraint(condition.metavar)) } - return ctx.containsMarkWithAnyStateBefore(edgeState, condition.metavar, position) + val contains = ctx.containsMarkWithAnyStateBefore(edgeState, condition.metavar, position) + if (!condition.star) return contains + // Starred sink ($*Y): match the base value OR any nested field carrying the mark. + val containsAnyField = + ctx.containsMarkOnAnyFieldWithAnyStateBefore(edgeState, condition.metavar, position) + return GoSerializedCondition.or(listOf(contains, containsAnyField)) } is ParamCondition.TypeIs -> { return ctx.goTypeMatcher(condition.typeName, semgrepRuleTrace) diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt index 6aa96e0d2..8c0c40016 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt @@ -43,7 +43,15 @@ internal data class GoEvaluatedEdgeCondition( internal data class GoRegisterVarPosition( val varName: MetavarAtom, - val positions: MutableSet, + val positions: MutableSet, +) + +// Mirrors Java's StarredPosition: carries whether the metavar occurrence was starred (`$*X`), so +// the emitter can add the any-accessor arm (assign/check/clean) alongside the base one. Go models +// "any field" as a distinct action/condition variant on the SAME position, not a position modifier. +internal data class GoStarredPosition( + val position: PositionBaseWithModifiers, + val star: Boolean, ) data class FieldModifierCtx( diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt index 5f52ea4af..e915ed949 100644 --- a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt @@ -626,6 +626,58 @@ class GoMassiveSampleTest : GoSampleBasedTestBase("GO_MASSIVE_SAMPLES_DIR") { @Test fun xss20VariadicSprintf() = runSampleDefault("xss_20_variadic_sprintf") - + + // ─── Star-operator ($*VAR) field-taint e2e samples (parity with Java StarSource/StarSink/StarSanitizer) ─── + + @Test + fun star01SinkField() = runSampleDefault("star_01_sink_field") + + @Test + fun star02SourceField() = runSampleDefault("star_02_source_field") + + @Test + fun star03SanitizerField() = runSampleDefault("star_03_sanitizer_field") + + // ─── Deep-nesting matrix: taint hidden 5+ fields deep and/or 5+ calls deep ─── + + @Test + fun star04DeepSinkField() = runSampleDefault("star_04_deep_sink_field") + + @Test + fun star05DeepSourceField() = runSampleDefault("star_05_deep_source_field") + + @Test + fun star06DeepSanitizerField() = runSampleDefault("star_06_deep_sanitizer_field") + + @Test + fun star07InterprocChain() = runSampleDefault("star_07_interproc_chain") + + @Test + fun star08SourceAndSink() = runSampleDefault("star_08_source_and_sink") + + // ─── Star matrix: 5x interproc depth combined with 5x field depth, one sample per star feature + // (parity with Java StarMatrix*) ─── + + @Test + fun star09MatrixSource() = runSampleDefault("star_09_matrix_source") + + @Test + fun star10MatrixSink() = runSampleDefault("star_10_matrix_sink") + + @Test + fun star11MatrixPropagator() = runSampleDefault("star_11_matrix_propagator") + + @Test + fun star12MatrixSanitizer() = runSampleDefault("star_12_matrix_sanitizer") + + @Test + fun star13MatrixPatternNot() = runSampleDefault("star_13_matrix_pattern_not") + + @Test + fun star14MatrixPatternInside() = runSampleDefault("star_14_matrix_pattern_inside") + + @Test + fun star15MatrixPatternNotInside() = runSampleDefault("star_15_matrix_pattern_not_inside") + private fun runSampleDefault(name: String) = runSample(name, useDefaultConfig = true) } diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/GoStarOperatorEmitTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/GoStarOperatorEmitTest.kt new file mode 100644 index 000000000..d87552192 --- /dev/null +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/GoStarOperatorEmitTest.kt @@ -0,0 +1,223 @@ +package org.opentaint.semgrep.pattern + +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedAssignAction +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCleanAction +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCondition +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedItem +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedRule +import org.opentaint.semgrep.go.pattern.conversion.GoLanguageStrategy +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Unit-level checks that the Go taint-rule emitter threads the `$*X` star operator, giving Go the + * same semantics as Java: `$X` = base-only taint; `$*X` = base + all nested fields (any-accessor). + * + * Mirrors [org.opentaint.semgrep.StarOperatorRuleGenTest] on the Java side, but the Go engine models + * "any field" as a distinct ACTION/CONDITION variant on the SAME position (AnyAccessor / + * ContainsMarkOnAnyAccessor), not as an AnyField position modifier. + */ +class GoStarOperatorEmitTest { + + private fun emitItems(ruleText: String): List { + val loader = SemgrepRuleLoader(listOf(GoLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), SemgrepLoadTrace()) + val (rule, _) = loader.loadRules().rulesWithMeta.single() + + @Suppress("UNCHECKED_CAST") + val taintRule = rule as TaintRuleFromSemgrep + return taintRule.taintRules.flatMap { it.rules } + } + + private fun flatten(c: GoSerializedCondition): List = when (c) { + is GoSerializedCondition.Or -> listOf(c) + c.anyOf.flatMap { flatten(it) } + is GoSerializedCondition.And -> listOf(c) + c.allOf.flatMap { flatten(it) } + is GoSerializedCondition.Not -> listOf(c) + flatten(c.not) + else -> listOf(c) + } + + private fun sinkConditions(items: List): List = + items.filterIsInstance() + .mapNotNull { it.condition } + .flatMap { flatten(it) } + + private fun sourceTaint(items: List): List = + items.filterIsInstance().flatMap { it.taint } + + private fun cleanerCleans(items: List): List = + items.filterIsInstance().flatMap { it.cleans } + + @Test + fun `starred sink checks base and any-accessor on the same arg`() { + val items = emitItems( + """ + rules: + - id: go-star-sink + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "util.Source(...)" + pattern-sinks: + - patterns: + - pattern: "util.Sink(${'$'}*Y)" + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = sinkConditions(items) + val base = conditions.filterIsInstance() + val anyAccessor = conditions.filterIsInstance() + + assertTrue(anyAccessor.isNotEmpty(), "expected a ContainsMarkOnAnyAccessor in the starred sink; got $conditions") + assertTrue(base.isNotEmpty(), "expected a plain ContainsMark in the starred sink; got $conditions") + + // Base coherence: each any-accessor check must be paired with a plain ContainsMark on the + // SAME mark and SAME position -- the two arms of an Or over the metavar's resolved position. + anyAccessor.forEach { af -> + assertTrue( + base.any { it.tainted == af.tainted && it.pos == af.pos }, + "any-accessor check $af has no paired plain ContainsMark on same mark/pos; base=$base" + ) + } + } + + @Test + fun `starred assignment-LHS source taints base and any-accessor`() { + val items = emitItems( + """ + rules: + - id: go-star-source + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "${'$'}*X = util.Source()" + pattern-sinks: + - pattern: "util.Sink(${'$'}Y)" + """.trimIndent() + ) + val taint = sourceTaint(items) + val direct = taint.filterIsInstance() + val anyAccessor = taint.filterIsInstance() + + assertTrue(direct.isNotEmpty(), "expected a Direct assign in the starred source; got $taint") + assertTrue(anyAccessor.isNotEmpty(), "expected an AnyAccessor assign in the starred source; got $taint") + + // Coherence: every any-accessor assign mirrors a direct one on the same mark and position. + anyAccessor.forEach { any -> + assertTrue( + direct.any { it.kind == any.kind && it.pos == any.pos }, + "any-accessor assign $any has no paired Direct on same mark/pos; direct=$direct" + ) + } + } + + @Test + fun `starred sanitizer cleans base and any-accessor`() { + val items = emitItems( + """ + rules: + - id: go-star-sanitizer + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "${'$'}X = util.Source()" + pattern-sanitizers: + - patterns: + - pattern: "util.Clean(${'$'}*X)" + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: "util.Sink(${'$'}X)" + """.trimIndent() + ) + val cleans = cleanerCleans(items) + val direct = cleans.filterIsInstance() + val anyAccessor = cleans.filterIsInstance() + + assertTrue(direct.isNotEmpty(), "expected a Direct clean in the starred sanitizer; got $cleans") + assertTrue(anyAccessor.isNotEmpty(), "expected an AnyAccessor clean in the starred sanitizer; got $cleans") + + // Coherence: every any-accessor clean mirrors a direct one on the same mark and position. + // The direct clean is REQUIRED: the any-accessor clean removes marks stored under an ANY + // accessor but does NOT reach a concrete base mark, so both must be emitted. + anyAccessor.forEach { any -> + assertTrue( + direct.any { it.taintKind == any.taintKind && it.pos == any.pos }, + "any-accessor clean $any has no paired Direct on same mark/pos; direct=$direct" + ) + } + } + + @Test + fun `starred TYPED sink checks base and any-accessor with the type constraint retained`() { + // `($*Y : string)` is a starred TYPED metavar. It lowers to And(IsMetavar(star), TypeIs), + // which the automata flattens into two per-position atoms; the starred IsMetavar atom must + // still thread the any-accessor arm, and the TypeIs atom must still emit an IsType check. + val items = emitItems( + """ + rules: + - id: go-star-typed-sink + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "util.Source(...)" + pattern-sinks: + - patterns: + - pattern: "util.Sink((${'$'}*Y : string))" + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = sinkConditions(items) + val base = conditions.filterIsInstance() + val anyAccessor = conditions.filterIsInstance() + + assertTrue(anyAccessor.isNotEmpty(), "expected a ContainsMarkOnAnyAccessor for the starred typed sink; got $conditions") + assertTrue(base.isNotEmpty(), "expected a plain ContainsMark for the starred typed sink; got $conditions") + assertTrue( + conditions.any { it is GoSerializedCondition.IsType }, + "expected the type constraint (IsType) to survive alongside the star; got $conditions" + ) + anyAccessor.forEach { af -> + assertTrue( + base.any { it.tainted == af.tainted && it.pos == af.pos }, + "any-accessor check $af has no paired plain ContainsMark on same mark/pos; base=$base" + ) + } + } + + @Test + fun `non-star sink checks base only`() { + val items = emitItems( + """ + rules: + - id: go-plain-sink + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "util.Source(...)" + pattern-sinks: + - patterns: + - pattern: "util.Sink(${'$'}Y)" + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = sinkConditions(items) + assertTrue( + conditions.none { it is GoSerializedCondition.ContainsMarkOnAnyAccessor }, + "a non-star \$Y sink must be base-only (no ContainsMarkOnAnyAccessor); got $conditions" + ) + assertTrue( + conditions.any { it is GoSerializedCondition.ContainsMark }, + "expected a base ContainsMark in the plain sink; got $conditions" + ) + } +} diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt index 396f65176..fa70c7969 100644 --- a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt @@ -108,6 +108,90 @@ class SemgrepGoPatternParserTest { assertNotNull(find(ast) { it is FuncDecl }) } + /** Collects every pattern node in the AST (self + descendants). */ + private fun collect(p: SemgrepGoPattern): List = + listOf(p) + p.children.flatMap { collect(it) } + + private fun metavars(pattern: String): List = + collect(parse(pattern)).filterIsInstance() + + @Test fun starredMetavarInCallArgument() { + val y = metavars("Sink(\$*Y)").single { it.name == "\$Y" } + assertTrue(y.star, "expected \$*Y to be starred") + } + + /** Star count tolerating a parse failure (a retired/invalid form yields no starred metavar). */ + private fun starCount(pattern: String): Int { + val r = parser.parseSemgrepGoPattern(pattern) + return if (r is SemgrepGoPatternParsingResult.Ok) + collect(r.pattern).filterIsInstance().count { it.star } + else 0 + } + + @Test fun prefixStarNotSuffixMarksTheMetavar() { + // The star is a `$*` prefix bound into the metavar token. `$Y * z` stays multiplication, + // and the retired suffix form `$Y*` is no longer a starred metavar. + assertEquals(1, starCount("Sink(\$*Y)"), "\$*Y must be a star") + assertEquals(0, starCount("Sink(\$Y * z)"), "\$Y * z must not be a star") + assertEquals(0, starCount("Sink(\$Y*)"), "retired suffix \$Y* must not be a star") + } + + @Test fun plainMetavarIsNotStarred() { + val y = metavars("Sink(\$Y)").single { it.name == "\$Y" } + assertTrue(!y.star, "plain \$Y must not be starred") + } + + @Test fun starredMetavarOnAssignmentLhs() { + val x = metavars("\$*X = Source()").single { it.name == "\$X" } + assertTrue(x.star, "expected LHS \$*X to be starred") + } + + private fun typedMetavars(pattern: String): List = + collect(parse(pattern)).filterIsInstance() + + @Test fun starredTypedMetavar() { + // `($*Y : SomeType)` parses to a starred typed metavar carrying its type constraint. + val tm = typedMetavars("Sink((\$*Y : SomeType))").single { it.name == "\$Y" } + assertTrue(tm.star, "expected (\$*Y : SomeType) to be a starred typed metavar") + } + + @Test fun plainTypedMetavarIsNotStarred() { + // `($Y : SomeType)` stays an unstarred typed metavar (byte-identical to before). + val tm = typedMetavars("Sink((\$Y : SomeType))").single { it.name == "\$Y" } + assertTrue(!tm.star, "plain (\$Y : SomeType) must not be starred") + } + + @Test fun retiredSuffixTypedMetavarIsNotStarred() { + // The retired suffix forms `($Y* : T)` and the spaced `($Y * : T)` no longer denote a + // starred typed metavar: the star is now a `$*` prefix, so neither parses as one. + for (p in listOf("Sink((\$Y* : SomeType))", "Sink((\$Y * : SomeType))")) { + val r = parser.parseSemgrepGoPattern(p) + val starred = r is SemgrepGoPatternParsingResult.Ok && + typedMetavars(p).any { it.star } + assertTrue(!starred, "`$p` must not parse as a starred typed metavar; got $r") + } + } + + @Test fun starredTypedReceiverParses() { + // Typed receiver form `($*C : *exec.Cmd).Run()` parses with both star and the type restored. + // The `*` in `*exec.Cmd` is a pointer type, distinct from the metavar's `$*` star prefix. + val tm = typedMetavars("(\$*C : *exec.Cmd).Run()").single { it.name == "\$C" } + assertTrue(tm.star, "expected (\$*C : *exec.Cmd) receiver to be a starred typed metavar") + } + + @Test fun prefixDerefStillParses() { + // `*p` is a prefix deref (STAR precedes the operand), not a starred metavar. + val ast = parse("*p") + assertEquals(0, collect(ast).filterIsInstance().count { it.star }) + } + + @Test fun binaryMulStillParses() { + // `a*b` is multiplication; no starred metavars and still a valid parse. + val ast = parse("a*b") + assertTrue(ast !is SemgrepGoPattern.Raw) + assertEquals(0, collect(ast).filterIsInstance().count { it.star }) + } + @Test fun structuralSmokeTest() { // 5 representative patterns -> AST non-Raw val patterns = listOf( diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt index 803d83984..98898eaed 100644 --- a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt @@ -12,11 +12,13 @@ import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModif import org.opentaint.dataflow.go.GoFunctionSignature import org.opentaint.dataflow.go.rules.GoTaintConfiguration import org.opentaint.dataflow.go.rules.Position +import org.opentaint.dataflow.go.rules.RemoveMark import org.opentaint.ir.go.type.GoIRUnsafePointerType import org.opentaint.semgrep.go.pattern.conversion.loadGoTaintConfiguration import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue class GoTaintRuleEmitterTest { @@ -143,6 +145,43 @@ class GoTaintRuleEmitterTest { assertEquals("util.Clean", cfg.cleanerForFunction("util.Clean".signature(1), allRelevant = false).single().function) } + @Test + fun `any-accessor cleaner lowers to RemoveMark with onAnyAccessor while direct stays false`() { + val pos = baseOnly(PositionBase.Argument(0)) + + val anyRule = rule( + GoSerializedRule.Cleaner( + pkg = GoNameMatcher.Simple("util"), + function = GoNameMatcher.Simple("Clean"), + cleans = listOf(GoSerializedCleanAction.AnyAccessor("taint", pos)), + info = null, + ), + ) + val anyCfg = GoTaintConfiguration().loadGoTaintConfiguration(anyRule) + val anyAction = anyCfg.cleanerForFunction("util.Clean".signature(1), allRelevant = false) + .single().actionsAfter.filterIsInstance().single() + assertEquals("taint", anyAction.mark) + assertTrue(anyAction.onAnyAccessor) + + // Direct variant via the companion constructor must stay byte-identical (onAnyAccessor = false). + val directRule = rule( + GoSerializedRule.Cleaner( + pkg = GoNameMatcher.Simple("util"), + function = GoNameMatcher.Simple("Clean"), + cleans = listOf(GoSerializedCleanAction("taint", pos)), + info = null, + ), + ) + val directCfg = GoTaintConfiguration().loadGoTaintConfiguration(directRule) + val directAction = directCfg.cleanerForFunction("util.Clean".signature(1), allRelevant = false) + .single().actionsAfter.filterIsInstance().single() + assertEquals("taint", directAction.mark) + assertFalse(directAction.onAnyAccessor) + + // Both variants resolve to the same base position; only the any-accessor flag differs. + assertEquals(directAction.pos, anyAction.pos) + } + private val anyType = GoIRUnsafePointerType fun String.signature(args: Int): GoFunctionSignature = diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSanitizer.java new file mode 100644 index 000000000..43cf9a78b --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSanitizer.java @@ -0,0 +1,50 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SANITIZER, taint hidden 5 fields deep. `clean($*C)` must clear the taint on the + * whole object INCLUDING nested fields at every depth, so a subsequent depth-5 field read is + * clean. Default AnyAccessorDisabled (matches StarSanitizer). + */ +@RuleSet("taint/StarDeepSanitizer.yaml") +public abstract class StarDeepSanitizer implements RuleSample { + String src() { return "tainted"; } + L0 clean(L0 b) { return b; } // $*C sanitizer: clears object + all fields at all depths + void sink(String data) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + private static L0 build() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + return o; + } + + // Positive: tainted depth-5 field reaches the sink with NO sanitizer between. + final static class PositiveTaintedDeep extends StarDeepSanitizer { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = src(); + sink(o.f.f.f.f.v); + } + } + + // Negative: the $*C sanitizer must clean the depth-5 field taint on the returned object. + final static class NegativeSanitizedDeep extends StarDeepSanitizer { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = src(); + L0 cleaned = clean(o); + sink(cleaned.f.f.f.f.v); // depth-5 field taint must be gone after $*C + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSink.java new file mode 100644 index 000000000..39fe8bb2d --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSink.java @@ -0,0 +1,82 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SINK, taint hidden at graduated field depths. A plain source taints a field N levels + * down; the starred whole-object sink `sink($*Y)` must observe the any-field taint. + * + * The deep cases were parked as KnownFn* while deep concrete field-store FACT PRODUCTION was + * broken (the IR lowers `o.f.v1 = src()` through a temp, so the fact was rooted at the temp + * and never at `o.f.v1` — see DeepFieldStoreFn). The upstream fix (49c8792b9, #304) makes the + * interprocedural precise READ work (DeepFieldStoreFn is green) and the DEPTH-5 starred-sink + * observation work (PositiveDepth5 live below). RESIDUAL GAP: the DEPTH-2 starred-sink + * observation still misses (stable repro, independent of the any-accessor unroll strategy) — + * KnownFnDepth2 stays parked, see its comment for the root cause. + */ +@RuleSet("taint/StarDeepSink.yaml") +public abstract class StarDeepSink implements RuleSample { + String src() { return "tainted"; } + void sink(L0 b) {} + + static final class L0 { public String v0; public L1 f; } + static final class L1 { public String v1; public L2 f; } + static final class L2 { public String v2; public L3 f; } + static final class L3 { public String v3; public L4 f; } + static final class L4 { public String v; } + + private static L0 build() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + return o; + } + + // Positive (WORKS): taint at field depth 1 — the starred sink observes it. + final static class PositiveDepth1 extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.v0 = src(); // depth-1 field + sink(o); + } + } + + // KNOWN FALSE NEGATIVE: the depth-2 concrete field mark is not observed by the starred sink, + // while the DEEPER depth-5 case works and the same store on a LOCALLY ALLOCATED base works + // (DeepFieldStoreFn). Root cause: the base comes from the opaque `build()` call, so the store + // is lowered to `%tmp = o.f; %tmp.v1 = src()` with `%tmp` live across the opaque `src()` call; + // DSUAliasAnalysis.invalidateOuterHeapAliases must break the live `%tmp ~ o.f` link there, and + // since the DSU cannot hold a singleton set the whole pair is dropped, so the tainted store + // through the temp is never rebased onto `o.f.v1`. Depth >= 3 escapes only by accident: those + // temps are dead at the call and dead-local cleanup has already orphaned the chain. + // See DSUAliasAnalysisInvalidateOuterHeapAliasesTest.invalidateDropsLiveHeapAliasLosingPathRelation, + // which pins the same loss at the alias-analysis level. Unpark both together. + final static class KnownFnDepth2 extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.f.v1 = src(); + sink(o); + } + } + + // Positive: depth-5 concrete field mark observed by the starred sink. + final static class PositiveDepth5 extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = src(); + sink(o); + } + } + + // Negative: no field ever tainted. + final static class NegativeCleanObject extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = "safe"; + sink(o); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSource.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSource.java new file mode 100644 index 000000000..018b2575a --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSource.java @@ -0,0 +1,56 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE, taint hidden 5 fields deep. `$*X = src()` taints the whole L0 object AND + * every nested field at every depth; a 5-level field read must still observe the taint once + * the any-accessor is unrolled to concrete field reads (AnyAccessorEnabled). + * + * Depth axis: field nesting L0.f.f.f.f.v (5 hops). Removing the source `*` makes every + * Positive a false negative, proving the star is load-bearing. + */ +@RuleSet("taint/StarDeepSource.yaml") +public abstract class StarDeepSource implements RuleSample { + L0 src() { return new L0(); } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Positive: starred source's any-field taint reaches a depth-5 field read. + final static class PositiveDeepFieldRead extends StarDeepSource { + @Override public void entrypoint() { + L0 o = src(); // $*X = src(): whole object + any-field taint + String v = o.f.f.f.f.v; // depth-5 read, any-accessor unrolled + sink(v); + } + } + + // Positive: read a shallower (depth-3) field — still tainted by the whole-object star. + final static class PositiveShallowFieldRead extends StarDeepSource { + @Override public void entrypoint() { + L0 o = src(); + L3 mid = o.f.f.f; // depth-3 read: a sub-object is still tainted + String v = mid.f.v; + sink(v); + } + } + + // Negative: object built locally, no source flows in, so no field is tainted. + final static class NegativeCleanDeep extends StarDeepSource { + @Override public void entrypoint() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + o.f.f.f.f.v = "safe"; + sink(o.f.f.f.f.v); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarInterproc.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarInterproc.java new file mode 100644 index 000000000..fa63b8f77 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarInterproc.java @@ -0,0 +1,68 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE threaded through a 5+ deep interprocedural call chain that ALTERNATELY hides + * taint inside an object and exposes it again. `$*X = src()` taints the whole Box + any-field; + * the chain unwraps to a scalar, re-wraps into a fresh Box field, unwraps again, and finally + * reaches a plain sink. Needs AnyAccessorEnabled so the source star reaches the first concrete + * field read. + */ +@RuleSet("taint/StarInterproc.yaml") +public abstract class StarInterproc implements RuleSample { + Box src() { return new Box(); } + void sink(String s) {} + + static final class Box { public String v; } + + // step1..step5: 5 interprocedural hops. Alternation: + // step1 pass object -> step2 EXPOSE field to scalar -> step3 HIDE scalar in new Box + // -> step4 pass object -> step5 EXPOSE field to scalar reaching the sink. + protected Box step1(Box b) { return b; } + protected String step2(Box b) { return b.v; } + protected Box step3(String s) { Box n = new Box(); n.v = s; return n; } + protected Box step4(Box b) { return b; } + protected String step5(Box b) { return b.v; } + + // Positive: taint survives 5 hops of hide/expose alternation from a starred source. + final static class PositiveAlternatingChain extends StarInterproc { + @Override public void entrypoint() { + Box b = src(); // $*X = src(): whole-object + any-field taint + Box b1 = step1(b); // hop 1: object passes through + String s2 = step2(b1); // hop 2: EXPOSE (any-field unrolls to b1.v) + Box b3 = step3(s2); // hop 3: HIDE the scalar back into a field + Box b4 = step4(b3); // hop 4: object passes through + String s5 = step5(b4); // hop 5: EXPOSE the field again + sink(s5); + } + } + + // Positive: simplest 5-hop pass-through, field exposed only at the end. + final static class PositivePassThroughChain extends StarInterproc { + @Override public void entrypoint() { + Box b = src(); + Box b1 = step1(b); + Box b2 = step1(b1); + Box b3 = step1(b2); + Box b4 = step4(b3); + String s = step5(b4); + sink(s); + } + } + + // Negative: fresh untainted Box threaded through the same chain. + final static class NegativeCleanChain extends StarInterproc { + @Override public void entrypoint() { + Box b = new Box(); + b.v = "safe"; + Box b1 = step1(b); + String s2 = step2(b1); + Box b3 = step3(s2); + Box b4 = step4(b3); + String s5 = step5(b4); + sink(s5); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternInside.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternInside.java new file mode 100644 index 000000000..1e1562fa0 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternInside.java @@ -0,0 +1,76 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred sink gated by PATTERN-INSIDE, 5+ interprocedural depth x 5+ field depth combined. + * The sink `$R.consume($*Y)` only counts when the receiver comes from `openSink()` in the same + * method (pattern-inside). A starred source five calls deep taints a whole L0; the object + * travels five hops; the consume call sits five calls deep. The gated method uses openSink() + * (flagged); the ungated one obtains its receiver elsewhere (not a sink at all). + */ +@RuleSet("taint/StarMatrixPatternInside.yaml") +public abstract class StarMatrixPatternInside implements RuleSample { + L0 src() { return new L0(); } + Out openSink() { return new Out(); } + Out plainOut() { return new Out(); } + + static final class Out { void consume(L0 o) {} } + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Sink chain five calls deep, ending in the pattern-inside-gated consume. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { + Out r = openSink(); // pattern-inside context + r.consume(o); // starred sink matches HERE, depth 5 + } + + // Same-depth chain whose consume receiver does NOT come from openSink(). + protected void j1(L0 o) { j2(o); } + protected void j2(L0 o) { j3(o); } + protected void j3(L0 o) { j4(o); } + protected void j4(L0 o) { j5(o); } + protected void j5(L0 o) { + Out r = plainOut(); // no pattern-inside context + r.consume(o); + } + + // Positive: tainted object consumed inside the gated context. + final static class PositiveGatedConsume extends StarMatrixPatternInside { + @Override public void entrypoint() { + L0 o = src5(); + k1(p5(p4(p3(p2(p1(o)))))); + } + } + + // Negative: same tainted object, but the consume call lacks the pattern-inside context. + final static class NegativeUngatedConsume extends StarMatrixPatternInside { + @Override public void entrypoint() { + L0 o = src5(); + j1(p5(p4(p3(p2(p1(o)))))); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNot.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNot.java new file mode 100644 index 000000000..a317d93d4 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNot.java @@ -0,0 +1,67 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred PATTERN-NOT sink, 5+ interprocedural depth x 5+ field depth combined. The sink is + * `emit($*Y, $MODE)` with `pattern-not: emit($*Y, "safe")` — the starred metavar occurrence + * appears in BOTH the pattern and the pattern-not (the constraint solver keeps $Y and $*Y + * distinct, so the forms must agree). A starred source five calls deep taints a whole L0; the + * object travels five hops and is emitted five calls deep — flagged in "html" mode, excluded + * by the pattern-not in "safe" mode. + */ +@RuleSet("taint/StarMatrixPatternNot.yaml") +public abstract class StarMatrixPatternNot implements RuleSample { + L0 src() { return new L0(); } + void emit(L0 o, String mode) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Two sink chains five calls deep: one emits in a flagged mode, one in the excluded mode. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { emit(o, "html"); } // matches the sink, depth 5 + + protected void j1(L0 o) { j2(o); } + protected void j2(L0 o) { j3(o); } + protected void j3(L0 o) { j4(o); } + protected void j4(L0 o) { j5(o); } + protected void j5(L0 o) { emit(o, "safe"); } // excluded by pattern-not, depth 5 + + // Positive: tainted object emitted in a non-excluded mode. + final static class PositiveEmitHtml extends StarMatrixPatternNot { + @Override public void entrypoint() { + L0 o = src5(); + k1(p5(p4(p3(p2(p1(o)))))); + } + } + + // Negative: same tainted object, but the emit call matches the pattern-not. + final static class NegativeEmitSafe extends StarMatrixPatternNot { + @Override public void entrypoint() { + L0 o = src5(); + j1(p5(p4(p3(p2(p1(o)))))); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNotInside.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNotInside.java new file mode 100644 index 000000000..f94d9d295 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNotInside.java @@ -0,0 +1,80 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred sink guarded by PATTERN-NOT-INSIDE, 5+ interprocedural depth x 5+ field depth + * combined. The sink `use($*Y)` sits in a `pattern-inside` context that INTRODUCES the guard + * receiver (`$G = checker(); ...`), and `pattern-not-inside: $G.check($*Y); ...` suppresses it + * — every not-inside metavar must be introduced and wired by the pattern-inside/sink patterns + * (the shipped setContentType suppression idiom; a not-inside with unbound metavars is dropped + * during automata-to-taint-rule conversion). A starred source five + * calls deep taints a whole L0; the object travels five hops; the use call sits five calls + * deep — flagged in the unguarded method, suppressed in the guarded one. + */ +@RuleSet("taint/StarMatrixPatternNotInside.yaml") +public abstract class StarMatrixPatternNotInside implements RuleSample { + L0 src() { return new L0(); } + void use(L0 o) {} + Checker checker() { return new Checker(); } + + static final class Checker { void check(L0 o) {} } + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Unguarded sink chain five calls deep. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { + Checker g = checker(); // pattern-inside context (binds $G), no check() -> flagged + use(o); // starred sink matches HERE, depth 5 + } + + // Guarded sink chain five calls deep: guard() precedes the use in the same method. + protected void j1(L0 o) { j2(o); } + protected void j2(L0 o) { j3(o); } + protected void j3(L0 o) { j4(o); } + protected void j4(L0 o) { j5(o); } + protected void j5(L0 o) { + Checker g = checker(); // pattern-inside context (binds $G) + g.check(o); // pattern-not-inside: $G.check($*Y) precedes -> suppressed + use(o); + } + + // Positive: tainted object used without the guard. + final static class PositiveUnguardedUse extends StarMatrixPatternNotInside { + @Override public void entrypoint() { + L0 o = src5(); + k1(p5(p4(p3(p2(p1(o)))))); + } + } + + // Negative: same tainted object, but the use is preceded by guard(). + final static class NegativeGuardedUse extends StarMatrixPatternNotInside { + @Override public void entrypoint() { + L0 o = src5(); + j1(p5(p4(p3(p2(p1(o)))))); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPropagator.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPropagator.java new file mode 100644 index 000000000..7f6459938 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPropagator.java @@ -0,0 +1,83 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred PROPAGATOR — BOTH occurrences starred (`$*T = pass($*F)`) — at 5+ interprocedural + * depth x 5+ field depth. A starred source five calls deep taints a whole L0; the object + * travels five pass-hops to the propagator call, whose starred FROM observes the any-field + * taint of the whole argument and whose starred TO assigns whole-object taint to the fresh + * M0 result. The M0 is then unwrapped ONE field level per hop across five calls + * (M0->..->String) — only possible if the TO really carries any-field taint — and the scalar + * travels five calls down a sink chain to a plain sink. + */ +@RuleSet("taint/StarMatrixPropagator.yaml") +public abstract class StarMatrixPropagator implements RuleSample { + L0 src() { return new L0(); } + M0 pass(L0 o) { return new M0(); } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + static final class M0 { public M1 f; } + static final class M1 { public M2 f; } + static final class M2 { public M3 f; } + static final class M3 { public M4 f; } + static final class M4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops before the propagator. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Five hops, each unwrapping one field level of the PROPAGATED object: taint reaches the + // scalar only if the starred TO assigned any-field taint to the M0. + protected M1 u1(M0 o) { return o.f; } + protected M2 u2(M1 o) { return o.f; } + protected M3 u3(M2 o) { return o.f; } + protected M4 u4(M3 o) { return o.f; } + protected String u5(M4 o) { return o.v; } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: deep source -> 5 hops -> starred propagator -> per-hop unwrap -> deep sink. + final static class PositivePropagatedDeep extends StarMatrixPropagator { + @Override public void entrypoint() { + L0 o = src5(); + L0 o5 = p5(p4(p3(p2(p1(o))))); + M0 t = pass(o5); // $*T = pass($*F): whole object in, whole object out + String s = u5(u4(u3(u2(u1(t))))); + k1(s); + } + } + + // Negative: an untainted object through the identical propagator and chains. + final static class NegativeCleanPropagated extends StarMatrixPropagator { + @Override public void entrypoint() { + L0 o = new L0(); + L0 o5 = p5(p4(p3(p2(p1(o))))); + M0 t = pass(o5); + String s = u5(u4(u3(u2(u1(t))))); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSanitizer.java new file mode 100644 index 000000000..14359b9e9 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSanitizer.java @@ -0,0 +1,69 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SANITIZER, 5+ interprocedural depth x 5+ field depth combined. A starred source five + * calls deep taints a whole L0. On the sanitized path the object goes through `sanitize()` — a + * HELPER whose body calls the starred-clean `clean()` (the wrapper shape behind the OWASP + * escapeHtml FPs, i.e. the deep-mark-exclusion fix's sample-level regression test). Afterwards + * five hops unwrap one field level each and the scalar travels five calls down to the sink; + * the whole-object clean must have removed the any-field taint at every depth. + */ +@RuleSet("taint/StarMatrixSanitizer.yaml") +public abstract class StarMatrixSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 clean(L0 o) { return o; } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // The starred clean sits INSIDE a wrapper: its whole-object effect must survive the + // wrapper's interprocedural summary (deep mark exclusions). + protected L0 sanitize(L0 o) { return clean(o); } + + // Five hops, each unwrapping exactly one field level. + protected L1 u1(L0 o) { return o.f; } + protected L2 u2(L1 o) { return o.f; } + protected L3 u3(L2 o) { return o.f; } + protected L4 u4(L3 o) { return o.f; } + protected String u5(L4 o) { return o.v; } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: the unsanitized path flags. + final static class PositiveUnsanitizedDeep extends StarMatrixSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } + + // Negative: the wrapped whole-object clean clears the taint at every field depth. + final static class NegativeSanitizedDeep extends StarMatrixSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + L0 c = sanitize(o); + String s = u5(u4(u3(u2(u1(c))))); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSink.java new file mode 100644 index 000000000..93a0033ac --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSink.java @@ -0,0 +1,63 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SINK, 5+ interprocedural depth x 5+ field depth combined. A starred source taints the + * INNERMOST object (L4) five calls deep; five hops then each WRAP it one level deeper + * (L4->L3->..->L0), and the outermost object travels five calls down a sink chain to + * `sink($*Y)` — the starred sink must observe the whole-object taint buried five field levels + * down the wrapped object. + */ +@RuleSet("taint/StarMatrixSink.yaml") +public abstract class StarMatrixSink implements RuleSample { + L4 src() { return new L4(); } + void sink(L0 o) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep: the starred source statement is inside src1. + protected L4 src5() { return src4(); } + protected L4 src4() { return src3(); } + protected L4 src3() { return src2(); } + protected L4 src2() { return src1(); } + protected L4 src1() { L4 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five hops, each WRAPPING one field level (hide direction). + protected L3 w1(L4 o) { L3 n = new L3(); n.f = o; return n; } + protected L2 w2(L3 o) { L2 n = new L2(); n.f = o; return n; } + protected L1 w3(L2 o) { L1 n = new L1(); n.f = o; return n; } + protected L0 w4(L1 o) { L0 n = new L0(); n.f = o; return n; } + protected L0 w5(L0 o) { return o; } + + // Sink five calls deep. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { sink(o); } // sink($*Y) matches HERE, depth 5 + + // Positive: the tainted L4 is wrapped five levels deep; the starred sink observes it. + final static class PositiveWrappedDeep extends StarMatrixSink { + @Override public void entrypoint() { + L4 t = src5(); + L0 o = w5(w4(w3(w2(w1(t))))); + k1(o); + } + } + + // Negative: an untainted L4 wrapped and threaded through the identical chains. + final static class NegativeCleanWrapped extends StarMatrixSink { + @Override public void entrypoint() { + L4 t = new L4(); + t.v = "safe"; + L0 o = w5(w4(w3(w2(w1(t))))); + k1(o); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSource.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSource.java new file mode 100644 index 000000000..cdc6d89bf --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSource.java @@ -0,0 +1,67 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE, 5+ interprocedural depth x 5+ field depth combined. The source statement + * `$*X = src()` sits FIVE calls deep (src1..src5); the tainted whole object then climbs back + * up and is unwrapped ONE field level per hop across five more calls (u1..u5, L0->..->String), + * and the scalar finally travels five calls down a sink chain (k1..k5) to a plain sink. + * The 5-level field taint is carried by the $* source's abstract any-field mark. + */ +@RuleSet("taint/StarMatrixSource.yaml") +public abstract class StarMatrixSource implements RuleSample { + L0 src() { return new L0(); } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep: the starred source statement is inside src1. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five hops, each unwrapping exactly one field level: interproc depth x field depth. + protected L1 u1(L0 o) { return o.f; } + protected L2 u2(L1 o) { return o.f; } + protected L3 u3(L2 o) { return o.f; } + protected L4 u4(L3 o) { return o.f; } + protected String u5(L4 o) { return o.v; } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: deep source -> 5x1-field unwrap hops -> deep sink. + final static class PositiveDeepChain extends StarMatrixSource { + @Override public void entrypoint() { + L0 o = src5(); + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } + + // Negative: an untainted object through the identical chains. + final static class NegativeCleanChain extends StarMatrixSource { + @Override public void entrypoint() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + o.f.f.f.f.v = "safe"; + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMixedExclusionSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMixedExclusionSanitizer.java new file mode 100644 index 000000000..0fbc22d54 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMixedExclusionSanitizer.java @@ -0,0 +1,108 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Mixed exclusion kinds on ONE flow: a starred whole-object clean (deep exclusions on the + * wrapper summary's initial fact) combined with a plain value clean (depth-1 exclusion via an + * ordinary sanitizer inside another summarized helper). The same caller initial fact is + * refined by BOTH summary applications; with lossy replace semantics the later plain + * refinement dropped the accumulated deep entry and could resurrect the cleaned whole-object + * mark (the always-propagate-deep-marks regression). + */ +@RuleSet("taint/StarMixedExclusionSanitizer.yaml") +public abstract class StarMixedExclusionSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 cleanAll(L0 o) { return o; } + String cleanValue(String s) { return s; } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public String v; } + + protected L0 srcWrapped() { L0 o = src(); return o; } + + // Starred clean behind a wrapper summary: the summary's initial fact acquires the DEEP + // exclusion. + protected L0 sanitizeAll(L0 o) { return cleanAll(o); } + + // Plain clean behind a wrapper summary: the refinement carries a PLAIN (depth-1) part. + protected String sanitizeValue(String s) { return cleanValue(s); } + + protected String unwrap(L0 o) { return o.f.v; } + + // Starred clean + constant store into the cleaned region inside ONE summarized helper: + // the deep exclusion and the safe store must compose — the store must not resurrect the + // cleaned whole-object mark on the returned object. Two store depths: the String leaf + // and the field itself (killing the whole subtree under f). + protected L0 sanitizeAllAndAssign(L0 o) { + cleanAll(o); + o.f.v = "safe"; + return o; + } + + protected L0 sanitizeAllAndAssignField(L0 o) { + cleanAll(o); + o.f = new L1(); + return o; + } + + // Positive: no sanitizer on the path. + final static class PositiveUnsanitized extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + sink(unwrap(o)); + } + } + + // Negative: starred clean, then the SAME flow continues through the plain-sanitizer + // summary as well — the later mixed refinement must keep the deep entry. + final static class NegativeStarThenPlain extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAll(o); + String s = unwrap(c); + String t = sanitizeValue(s); + sink(t); + } + } + + // Negative: starred clean alone through the wrapper — deep exclusion baseline. + final static class NegativeStarOnly extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAll(o); + sink(unwrap(c)); + } + } + + // Negative: starred clean followed by a constant store into the cleaned region, both + // behind one helper summary. + final static class NegativeStarThenAssign extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAllAndAssign(o); + sink(unwrap(c)); + } + } + + // Negative: starred clean followed by a field-level overwrite (fresh subtree), both + // behind one helper summary. + final static class NegativeStarThenAssignField extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAllAndAssignField(o); + sink(unwrap(c)); + } + } + + // Negative: plain clean alone — depth-1 exclusion baseline. + final static class NegativePlainOnly extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + String s = unwrap(o); + sink(sanitizeValue(s)); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarNestedWrapperSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarNestedWrapperSanitizer.java new file mode 100644 index 000000000..c3d89cffe --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarNestedWrapperSanitizer.java @@ -0,0 +1,108 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Summary COMPOSITION regressions for whole-object sanitizer cleans (deep mark exclusions): + * every way a starred clean can hide behind summary levels must stay effective. + * + * - NegativeSanitizedTwoWrappers: the clean under TWO nested wrapper summaries + * (sanitize2 -> sanitize1 -> clean), unwrap flow in the caller — the deep exclusion on each + * summary's initial fact prunes the caller's whole-object mark at delta application. + * - NegativeSanitizedInHelper / NegativeSanitizedNested: the clean AND the sinkward unwrap + * flow inside a summarized helper — the exclusion must survive being carried through the + * helper's own summary. This requires monotone (union, not replace) initial-fact exclusion + * refinement and the deep-entry carry on the delta-application path + * (MethodCallSummaryHandler); with lossy replace semantics a later application through an + * exclusion-free passthrough edge downgraded the refined initial and resurrected the + * cleaned mark — the historic false positive here. + */ +@RuleSet("taint/StarNestedWrapperSanitizer.yaml") +public abstract class StarNestedWrapperSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 clean(L0 o) { return o; } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // The starred clean under TWO wrapper summaries. + protected L0 sanitize1(L0 o) { return clean(o); } + protected L0 sanitize2(L0 o) { return sanitize1(o); } + + // Five hops, each unwrapping exactly one field level. + protected L1 u1(L0 o) { return o.f; } + protected L2 u2(L1 o) { return o.f; } + protected L3 u3(L2 o) { return o.f; } + protected L4 u4(L3 o) { return o.f; } + protected String u5(L4 o) { return o.v; } + + // The whole sanitized flow inside one more summarized helper. + protected String helper(L0 o) { + L0 c = sanitize2(o); + return u5(u4(u3(u2(u1(c))))); + } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: the unsanitized path flags. + final static class PositiveUnsanitizedNested extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } + + // Negative: clean + unwrap flow inside a summarized helper, two wrapper levels above the + // clean. + final static class NegativeSanitizedNested extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = helper(o); + k1(s); + } + } + + // Negative: two wrapper levels, flow at the entrypoint — the deep exclusion composes + // across nested wrapper summaries. + final static class NegativeSanitizedTwoWrappers extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + L0 c = sanitize2(o); + String s = u5(u4(u3(u2(u1(c))))); + k1(s); + } + } + + // Negative: ONE wrapper level (like StarMatrixSanitizer), with the sanitized flow itself + // inside a summarized helper. + protected String helperOneWrapper(L0 o) { + L0 c = sanitize1(o); + return u5(u4(u3(u2(u1(c))))); + } + + final static class NegativeSanitizedInHelper extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = helperOneWrapper(o); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSanitizer.java new file mode 100644 index 000000000..aa645e810 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSanitizer.java @@ -0,0 +1,31 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("taint/StarSanitizer.yaml") +public abstract class StarSanitizer implements RuleSample { + String src() { return "tainted"; } + static final class Box { String value; String getValue() { return value; } } + Box clean(Box b) { return b; } // $*C sanitizer: cleans the object + all its fields + void sink(String data) {} + + // Positive: tainted field reaches sink with NO sanitizer between + final static class PositiveTaintedField extends StarSanitizer { + @Override public void entrypoint() { + Box b = new Box(); + b.value = src(); + sink(b.getValue()); + } + } + + // Negative: the $*C sanitizer must clean the field taint on the value flowing onward + final static class NegativeSanitizedField extends StarSanitizer { + @Override public void entrypoint() { + Box b = new Box(); + b.value = src(); + Box cleaned = clean(b); + sink(cleaned.getValue()); // field taint must be gone after $*C sanitizer + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSink.java new file mode 100644 index 000000000..5261f0291 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSink.java @@ -0,0 +1,28 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("taint/StarSink.yaml") +public abstract class StarSink implements RuleSample { + String src() { return "tainted"; } + static final class Box { String value; } + void sink(Box b) {} + + final static class PositiveTaintedField extends StarSink { + @Override public void entrypoint() { + String data = src(); + Box b = new Box(); + b.value = data; // taints a field + sink(b); // $*Y sink fires on tainted field + } + } + + final static class NegativeCleanObject extends StarSink { + @Override public void entrypoint() { + Box b = new Box(); + b.value = "safe"; + sink(b); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSource.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSource.java new file mode 100644 index 000000000..177a10b3e --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSource.java @@ -0,0 +1,38 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("taint/StarSource.yaml") +public abstract class StarSource implements RuleSample { + Box src() { return new Box(); } + void sink(String s) {} + + static final class Box { + private String value; + String getValue() { return value; } + void setValue(String value) { this.value = value; } + } + + // Positive: the STARRED source ($*X = src()) taints the whole Box AND every field. + // The concrete field read b.getValue() therefore inherits the taint (the source-star's + // any-field taint is unrolled to the field read) and reaches the plain sink. + final static class PositiveStarredSourceField extends StarSource { + @Override public void entrypoint() { + Box b = src(); // $*X = src(): whole-object + any-field taint + String v = b.getValue(); // any-accessor taint unrolls to the concrete field + sink(v); // plain sink observes the tainted field + } + } + + // Negative: the Box is built locally (not from the starred source), so no field is + // tainted and the extracted value stays clean. + final static class NegativeCleanField extends StarSource { + @Override public void entrypoint() { + Box b = new Box(); + b.setValue("safe"); + String v = b.getValue(); + sink(v); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSanitizer.java new file mode 100644 index 000000000..06bd36e25 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSanitizer.java @@ -0,0 +1,40 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE + starred SANITIZER: `$*X = src()` taints the whole object and every nested + * field; `clean($*C)` must clear the whole object including nested fields. A depth-4 field read + * follows. Uses AnyAccessorEnabled so the source star reaches the concrete field read. + */ +@RuleSet("taint/StarSourceAndSanitizer.yaml") +public abstract class StarSourceAndSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 clean(L0 b) { return b; } + void sink(String data) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public String v; } + + // Positive: starred-source field taint reaches the sink with NO sanitizer between. + final static class PositiveDeepUnsanitized extends StarSourceAndSanitizer { + @Override public void entrypoint() { + L0 o = src(); // $*X whole-object taint + String v = o.f.f.f.v; // depth-4 read + sink(v); + } + } + + // Negative: the starred sanitizer clears the whole-object taint before the field read. + final static class NegativeDeepSanitized extends StarSourceAndSanitizer { + @Override public void entrypoint() { + L0 o = src(); + L0 cleaned = clean(o); // $*C clears object + all nested fields + String v = cleaned.f.f.f.v; + sink(v); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSink.java new file mode 100644 index 000000000..3590d49d9 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSink.java @@ -0,0 +1,40 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * BOTH ends starred: `$*X = src()` (whole-object source) and `sink($*Y)` (whole-object sink), + * with a nested object extracted in between. The source star taints every field of the outer + * object; a nested sub-object is pulled out and handed to the starred sink, which must observe + * it as tainted. Uses AnyAccessorEnabled so the source star reaches the extracted sub-object. + */ +@RuleSet("taint/StarSourceAndSink.yaml") +public abstract class StarSourceAndSink implements RuleSample { + Outer src() { return new Outer(); } + void sink(Inner i) {} + + static final class Outer { public Mid f; } + static final class Mid { public Inner f; } + static final class Inner { public String v; } + + // Positive: whole-object source taint reaches a nested sub-object handed to the starred sink. + final static class PositiveNestedObjectToStarSink extends StarSourceAndSink { + @Override public void entrypoint() { + Outer o = src(); // $*X: whole object + any-field taint + Inner inner = o.f.f; // extract a depth-2 nested object + sink(inner); // $*Y: starred sink observes the tainted sub-object + } + } + + // Negative: locally-built object, nothing tainted. + final static class NegativeCleanNested extends StarSourceAndSink { + @Override public void entrypoint() { + Outer o = new Outer(); + o.f = new Mid(); + o.f.f = new Inner(); + o.f.f.v = "safe"; + sink(o.f.f); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSanitizer.yaml new file mode 100644 index 000000000..2eebd38e8 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarDeepSanitizer + languages: + - java + severity: ERROR + message: match taint/StarDeepSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSink.yaml new file mode 100644 index 000000000..e5e127795 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarDeepSink + languages: + - java + severity: ERROR + message: match taint/StarDeepSink + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSource.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSource.yaml new file mode 100644 index 000000000..916c66ff7 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSource.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarDeepSource + languages: + - java + severity: ERROR + message: match taint/StarDeepSource + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarInterproc.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarInterproc.yaml new file mode 100644 index 000000000..5b449e7e1 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarInterproc.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarInterproc + languages: + - java + severity: ERROR + message: match taint/StarInterproc + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternInside.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternInside.yaml new file mode 100644 index 000000000..0459d0a99 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternInside.yaml @@ -0,0 +1,18 @@ +rules: + - id: taint-StarMatrixPatternInside + languages: + - java + severity: ERROR + message: match taint/StarMatrixPatternInside + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern-inside: | + $R = openSink(); + ... + - pattern: $R.consume($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNot.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNot.yaml new file mode 100644 index 000000000..fb54256dc --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNot.yaml @@ -0,0 +1,16 @@ +rules: + - id: taint-StarMatrixPatternNot + languages: + - java + severity: ERROR + message: match taint/StarMatrixPatternNot + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: emit($*Y, $MODE); + - pattern-not: emit($*Y, "safe"); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNotInside.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNotInside.yaml new file mode 100644 index 000000000..7493244e5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNotInside.yaml @@ -0,0 +1,21 @@ +rules: + - id: taint-StarMatrixPatternNotInside + languages: + - java + severity: ERROR + message: match taint/StarMatrixPatternNotInside + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern-inside: | + $G = checker(); + ... + - pattern: use($*Y); + - pattern-not-inside: | + $G.check($*Y); + ... + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPropagator.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPropagator.yaml new file mode 100644 index 000000000..fc138f507 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPropagator.yaml @@ -0,0 +1,20 @@ +rules: + - id: taint-StarMatrixPropagator + languages: + - java + severity: ERROR + message: match taint/StarMatrixPropagator + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-propagators: + - patterns: + - pattern: $*T = pass($*F); + from: $F + to: $T + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSanitizer.yaml new file mode 100644 index 000000000..ddf493144 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarMatrixSanitizer + languages: + - java + severity: ERROR + message: match taint/StarMatrixSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSink.yaml new file mode 100644 index 000000000..42811f6c5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarMatrixSink + languages: + - java + severity: ERROR + message: match taint/StarMatrixSink + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSource.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSource.yaml new file mode 100644 index 000000000..0a1cabc16 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSource.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarMatrixSource + languages: + - java + severity: ERROR + message: match taint/StarMatrixSource + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMixedExclusionSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMixedExclusionSanitizer.yaml new file mode 100644 index 000000000..01b9f68a5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMixedExclusionSanitizer.yaml @@ -0,0 +1,22 @@ +rules: + - id: taint-StarMixedExclusionSanitizer + languages: + - java + severity: ERROR + message: match taint/StarMixedExclusionSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: cleanAll($*C); + - focus-metavariable: $C + - patterns: + - pattern: cleanValue($V); + - focus-metavariable: $V + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarNestedWrapperSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarNestedWrapperSanitizer.yaml new file mode 100644 index 000000000..38bd2ed02 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarNestedWrapperSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarNestedWrapperSanitizer + languages: + - java + severity: ERROR + message: match taint/StarNestedWrapperSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSanitizer.yaml new file mode 100644 index 000000000..b9a08969f --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarSanitizer + languages: + - java + severity: ERROR + message: match taint/StarSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSink.yaml new file mode 100644 index 000000000..f36937634 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarSink + languages: + - java + severity: ERROR + message: match taint/StarSink + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSource.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSource.yaml new file mode 100644 index 000000000..202688c19 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSource.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarSource + languages: + - java + severity: ERROR + message: match taint/StarSource + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSanitizer.yaml new file mode 100644 index 000000000..a2ffbb951 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarSourceAndSanitizer + languages: + - java + severity: ERROR + message: match taint/StarSourceAndSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSink.yaml new file mode 100644 index 000000000..0647f9760 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarSourceAndSink + languages: + - java + severity: ERROR + message: match taint/StarSourceAndSink + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 b/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 index fea4795ea..1a633dc4a 100644 --- a/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 +++ b/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 @@ -212,6 +212,8 @@ LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN); IDENTIFIER: LetterNoDollar LetterOrDigit*; +STARRED_METAVAR: [$] '*' MetavarFirstLetter MetavarLetter*; + METAVAR: [$] MetavarFirstLetter MetavarLetter*; ANONYMOUS_METAVAR: [$] '_'; diff --git a/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 b/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 index 1d6a1bbb7..328744133 100644 --- a/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 +++ b/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 @@ -253,7 +253,8 @@ variableDeclarator ; variableDeclaratorId - : identifier ('[' ']')* + : STARRED_METAVAR + | identifier ('[' ']')* ; variableInitializer @@ -748,7 +749,7 @@ deepEllipsisExpression ; typedVariableExpression - : typeTypeOrVoid identifier + : typeTypeOrVoid (identifier | STARRED_METAVAR) ; // Java17 @@ -781,6 +782,7 @@ primary | thisExpression #PrimarySimple | SUPER #PrimarySimple | literal #PrimarySimple + | STARRED_METAVAR #PrimaryStarredMetavar | identifier #PrimarySimple | typeTypeOrVoid '.' CLASS #PrimaryClassLiteral | ellipsisExpression #PrimarySimple diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt index 6491ad5f9..28aec37bd 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt @@ -8,7 +8,7 @@ data object AnonymousMetavar : SemgrepJavaPattern { override val children: List = emptyList() } -data class Metavar(val name: String) : SemgrepJavaPattern { +data class Metavar(val name: String, val star: Boolean = false) : SemgrepJavaPattern { override val children: List = emptyList() } @@ -16,7 +16,7 @@ data class EllipsisMetavar(val name: String) : SemgrepJavaPattern { override val children: List = emptyList() } -data class TypedMetavar(val name: String, val type: TypeName) : SemgrepJavaPattern { +data class TypedMetavar(val name: String, val type: TypeName, val star: Boolean = false) : SemgrepJavaPattern { override val children: List = emptyList() } @@ -148,8 +148,10 @@ data class FormalArgument( val name: Name, val type: TypeName, val modifiers: List, + val star: Boolean = false, ) : SemgrepJavaPattern { - override val children: List = emptyList() + override val children: List = + if (star && name is MetavarName) listOf(Metavar(name.metavarName, star = true)) else emptyList() } data class NamedValue( diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt index 24df8f9a6..bd323e63c 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt @@ -63,6 +63,21 @@ import org.opentaint.semgrep.pattern.antlr.JavaParserBaseVisitor import java.util.Collections import java.util.IdentityHashMap +/** + * Parses a single Java semgrep pattern string into a [SemgrepJavaPattern], throwing on failure. + * + * Thin convenience wrapper over [SemgrepJavaPatternParser.parseSemgrepJavaPattern] using the exact + * same parser path the rule loader uses (see conversion/SemgrepPatternParser.kt). + */ +fun parseJavaSemgrepPattern(pattern: String): SemgrepJavaPattern = + when (val result = SemgrepJavaPatternParser().parseSemgrepJavaPattern(pattern)) { + is SemgrepJavaPatternParsingResult.Ok -> result.pattern + is SemgrepJavaPatternParsingResult.ParserFailure -> throw result.exception + is SemgrepJavaPatternParsingResult.OtherFailure -> throw result.exception + is SemgrepJavaPatternParsingResult.FailedASTParsing -> + error("Failed to parse pattern '$pattern': ${result.errorMessages}") + } + sealed interface SemgrepJavaPatternParsingResult { data class Ok(val pattern: SemgrepJavaPattern) : SemgrepJavaPatternParsingResult data class ParserFailure(val exception: SemgrepParsingException) : SemgrepJavaPatternParsingResult @@ -127,6 +142,15 @@ private fun IdentifierContext.parseName(): Name = withRule { return ConcreteName(text) } +// The starred `variableDeclaratorId` alternative (`STARRED_METAVAR`, i.e. `$*VAR`) has no +// `identifier` subrule. Returns the metavar name (`$VAR`, star prefix stripped) for that +// alternative, or null for the plain `identifier ('[' ']')*` one. +private fun JavaParser.VariableDeclaratorIdContext.starredMetavarName(): String? = + if (identifier() == null) STARRED_METAVAR().text.stripStar() else null + +// `$*VAR` -> `$VAR`: drop the `*` that follows the leading `$`, yielding the plain metavar name. +private fun String.stripStar(): String = "$" + substring(2) + private fun TypeIdentifierContext.parseTypeIdentifierName(): Name = withRule { tryRule(TypeIdentifierContext::METAVAR) { return MetavarName(it.text) } tryRule(TypeIdentifierContext::ANONYMOUS_METAVAR) { this@parseTypeIdentifierName.todo() } @@ -278,6 +302,9 @@ private class SemgrepJavaPatternParserVisitor : JavaParserBaseVisitor val type = value(FormalParameterContext::typeType).accept(typenameParser) ?: ctx.parsingFailed() val modifiers = value(FormalParameterContext::variableModifier).mapNotNull { parseModifier(it) } + + // Starred declarator alternative `STARRED_METAVAR` (`$*VAR`): the `identifier` subrule is absent. + declaratorId.starredMetavarName()?.let { starName -> + return FormalArgument(MetavarName(starName), type, modifiers, star = true) + } + + val name = declaratorId.identifier().parseName() return FormalArgument(name, type, modifiers) } unreachable() @@ -684,6 +720,9 @@ private class SemgrepJavaPatternParserVisitor : JavaParserBaseVisitor - createFormalArgument(newName, newType, newModifiers) + createFormalArgument(newName, newType, newModifiers, star) } } @@ -236,7 +236,7 @@ interface PatternRewriter { createStringLiteral(content.rewriteName()) fun TypedMetavar.rewriteTypedMetavar(): List = - createTypedMetavar(name, type.rewriteTypeName()) + createTypedMetavar(name, type.rewriteTypeName(), star) fun VariableAssignment.rewriteVariableAssignment(): List { val newType = type?.rewriteTypeName() @@ -296,8 +296,13 @@ interface PatternRewriter { fun createArrayAccess(obj: SemgrepJavaPattern, idx: SemgrepJavaPattern): List = listOf(ArrayAccess(obj, idx)) - fun createFormalArgument(name: Name, type: TypeName, modifiers: List): List = - listOf(FormalArgument(name, type, modifiers)) + fun createFormalArgument( + name: Name, + type: TypeName, + modifiers: List, + star: Boolean = false + ): List = + listOf(FormalArgument(name, type, modifiers, star)) fun createMethodDeclaration( name: Name, @@ -329,7 +334,8 @@ interface PatternRewriter { fun createReturnStmt(value: SemgrepJavaPattern?): List = listOf(ReturnStmt(value)) fun createStringLiteral(content: Name): List = listOf(StringLiteral(content)) - fun createTypedMetavar(name: String, type: TypeName): List = listOf(TypedMetavar(name, type)) + fun createTypedMetavar(name: String, type: TypeName, star: Boolean = false): List = + listOf(TypedMetavar(name, type, star)) fun createVariableAssignment( type: TypeName?, diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt index 2630c0e70..1564b197a 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt @@ -142,7 +142,7 @@ class PatternToActionListConverter: ActionListBuilder { } is Metavar -> { - IsMetavar(MetavarAtom.create(pattern.name)) + IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star) } is AnonymousMetavar -> { @@ -157,7 +157,7 @@ class PatternToActionListConverter: ActionListBuilder { val typeName = transformTypeName(pattern.type) ParamCondition.And( listOf( - IsMetavar(MetavarAtom.create(pattern.name)), + IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star), ParamCondition.TypeIs(typeName) ) ) @@ -182,12 +182,14 @@ class PatternToActionListConverter: ActionListBuilder { transformationFailed("Array access index is not ellipsis") } - when (pattern.obj) { - is Metavar, - is TypedMetavar -> { - // todo: dirty hack. We can ignore array access here due to the `hackResultArray` in taint configuration - return transformPatternIntoParamCondition(pattern.obj) - } + when (val obj = pattern.obj) { + // `$X[...]` array-element access: we don't model the concrete index. Force the + // metavar starred so it compiles to whole-object / any-field taint, which + // subsumes array-element (`[*]`) taint via the any-accessor machinery — the same + // mechanism that now backs array/vararg sinks in the taint config (the old + // runtime array-element condition reader has been replaced by an any-field check). + is Metavar -> return transformPatternIntoParamCondition(obj.copy(star = true)) + is TypedMetavar -> return transformPatternIntoParamCondition(obj.copy(star = true)) else -> transformationFailed("Array access object is not metavar") } } @@ -441,11 +443,11 @@ class PatternToActionListConverter: ActionListBuilder { when (val v = pattern.variable) { is Metavar -> { - conditions += IsMetavar(MetavarAtom.create(v.name)) + conditions += IsMetavar(MetavarAtom.create(v.name), star = v.star) } is TypedMetavar -> { - conditions += IsMetavar(MetavarAtom.create(v.name)) + conditions += IsMetavar(MetavarAtom.create(v.name), star = v.star) val typeName = transformTypeName(v.type) conditions += ParamCondition.TypeIs(typeName) @@ -597,7 +599,7 @@ class PatternToActionListConverter: ActionListBuilder { is MetavarName -> { paramConditions += ParamPattern( position, - IsMetavar(MetavarAtom.create(name.metavarName)) + IsMetavar(MetavarAtom.create(name.metavarName), star = param.star) ) } is AnonymousName -> {} diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/MethodFormulaManager.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/MethodFormulaManager.kt index 76ca5008d..1d3112ea9 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/MethodFormulaManager.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/MethodFormulaManager.kt @@ -6,6 +6,7 @@ import org.opentaint.semgrep.pattern.conversion.automata.MethodFormula.Or import org.opentaint.semgrep.pattern.conversion.automata.MethodFormula.True class MethodFormulaManager(initialPredicates: List = emptyList()) { + private val predicateIds = hashMapOf().also { initialPredicates.forEachIndexed { index, predicate -> it[predicate] = index diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt index c710244fa..e8d5db415 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt @@ -226,7 +226,7 @@ private fun Predicate.transform(context: MetavarUnificationContext): Predicate { val condition = constraint.condition val newCondition = when (condition) { - is IsMetavar -> IsMetavar(context.transform(condition.metavar)) + is IsMetavar -> IsMetavar(context.transform(condition.metavar), condition.star) is StringValueMetaVar -> StringValueMetaVar(context.transform(condition.metaVar)) else -> return this } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt index 3a99ff867..e73be44d0 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt @@ -123,7 +123,8 @@ private fun RuleConversionCtx.convertAutomataToTaint return ctx.createRuleGroup(rules) } -private data class RegisterVarPosition(val varName: MetavarAtom, val positions: MutableSet) +private data class StarredPosition(val position: PositionBase, val star: Boolean) +private data class RegisterVarPosition(val varName: MetavarAtom, val positions: MutableSet) private data class RuleCondition( val enclosingClassPackage: SerializedSimpleNameMatcher, @@ -184,6 +185,7 @@ private fun SerializedCondition.rewriteAsEndCondition(): SerializedCondition = w is SerializedCondition.IsNull -> copy(isNull = isNull.rewriteAsEndPosition()) is SerializedCondition.ConstantMatches -> copy(pos = pos.rewriteAsEndPosition()) is SerializedCondition.ContainsMark -> copy(pos = pos.rewriteAsEndPosition()) + is SerializedCondition.ContainsMarkOnAnyField -> copy(pos = pos.rewriteAsEndPosition()) is SerializedCondition.IsConstant -> copy(isConstant = isConstant.rewriteAsEndPosition()) is SerializedCondition.IsType -> copy(pos = pos.rewriteAsEndPosition()) is SerializedCondition.ParamAnnotated -> copy(pos = pos.rewriteAsEndPosition()) @@ -371,8 +373,14 @@ private fun JavaTaintRuleGenerationCtx.buildStateAssignAction( val requiredVariables = state.register.assignedVars.keys val result = requiredVariables.flatMapTo(mutableListOf()) { varName -> val varPosition = edgeCondition.accessedVarPosition[varName] ?: return@flatMapTo emptyList() - varPosition.positions.flatMap { - stateAssignMark(varPosition.varName, state, it.base()) + varPosition.positions.flatMap { sp -> + val base = sp.position.base() + val assigns = stateAssignMark(varPosition.varName, state, base) + if (sp.star) { + assigns + stateAssignMark(varPosition.varName, state, base.withAnyField()) + } else { + assigns + } } } @@ -389,8 +397,14 @@ private fun JavaTaintRuleGenerationCtx.buildStateCleanAction( edgeCondition: EvaluatedEdgeCondition ): List { val result = edgeCondition.accessedVarPosition.values.flatMapTo(mutableListOf()) { varPosition -> - varPosition.positions.flatMap { - stateCleanMark(varPosition.varName, state, stateBefore, it.base()) + varPosition.positions.flatMap { sp -> + val base = sp.position.base() + val cleans = stateCleanMark(varPosition.varName, state, stateBefore, base) + if (sp.star) { + cleans + stateCleanMark(varPosition.varName, state, stateBefore, base.withAnyField()) + } else { + cleans + } } } @@ -413,8 +427,11 @@ private fun EvaluatedEdgeCondition.addStateCheck( stateChecks += ctx.globalStateMarkName(state).mkContainsMark(ctx.stateVarPosition) } else { for (metaVar in state.register.assignedVars.keys) { - for (pos in accessedVarPosition[metaVar]?.positions.orEmpty()) { - stateChecks += ctx.containsStateMark(metaVar, state, pos.base()) + for (sp in accessedVarPosition[metaVar]?.positions.orEmpty()) { + stateChecks += ctx.containsStateMark(metaVar, state, sp.position.base()) + if (sp.star) { + stateChecks += ctx.containsStateMarkOnAnyField(metaVar, state, sp.position.base()) + } } } } @@ -1103,7 +1120,7 @@ private fun findMetaVarPosition( val varPosition = varPositions.getOrPut(condition.metavar) { RegisterVarPosition(condition.metavar, hashSetOf()) } - varPosition.positions.add(position) + varPosition.positions.add(StarredPosition(position, condition.star)) } private fun JavaTaintRuleGenerationCtx.evaluateParamCondition( @@ -1122,7 +1139,12 @@ private fun JavaTaintRuleGenerationCtx.evaluateParamCondition( semgrepRuleTrace.error(IgnoredMetavarConstraint(condition.metavar)) } - return containsMarkWithAnyStateBefore(state, condition.metavar, position.base()) + val contains = containsMarkWithAnyStateBefore(state, condition.metavar, position.base()) + if (!condition.star) return contains + + val containsAnyField = + containsMarkOnAnyFieldWithAnyStateBefore(state, condition.metavar, position.base()) + return taintRuleStrategy.conditionBuilder.or(listOf(contains, containsAnyField)) } is ParamCondition.TypeIs -> { diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt index 776ca7e30..6320c5ee4 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt @@ -343,7 +343,7 @@ data class StringConcatCtx( return when (condition) { is IsMetavar -> { val newMetavars = metavarMapping[condition.metavar] ?: return listOf(condition) - val modified = newMetavars.map(::IsMetavar) + val modified = newMetavars.map { IsMetavar(it, condition.star) } if (condition.metavar !in newMetavars || newMetavars.size > 1) { return modified + ParamCondition.TypeIs(stringType) @@ -393,7 +393,7 @@ fun eliminateStringConcat( val predCondition = it.asConditionOnStringConcat() ?: return@any false - check(predCondition == IsMetavar(metavar)) { "Unexpected condition" } + check(predCondition is IsMetavar && predCondition.metavar == metavar) { "Unexpected condition" } !it.negated } @@ -405,7 +405,7 @@ fun eliminateStringConcat( val predCondition = it.asConditionOnStringConcat() ?: return@any false - check(predCondition == IsMetavar(metavar)) { "Unexpected condition" } + check(predCondition is IsMetavar && predCondition.metavar == metavar) { "Unexpected condition" } !it.negated } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt index 38da9b511..520a4c692 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt @@ -11,7 +11,6 @@ import org.opentaint.semgrep.pattern.MetaVarConstraintFormulaCube import org.opentaint.semgrep.pattern.MetaVarConstraints import org.opentaint.semgrep.pattern.ResolvedMetaVarInfo import org.opentaint.semgrep.pattern.conversion.IsMetavar -import org.opentaint.semgrep.pattern.conversion.MetavarAtom import org.opentaint.semgrep.pattern.conversion.ParamCondition.Atom import org.opentaint.semgrep.pattern.conversion.LanguageTypeOps import org.opentaint.semgrep.pattern.conversion.SemgrepPatternAction.SignatureName @@ -282,7 +281,9 @@ fun MethodFormulaManager.simplifyMethodFormulaCube( typeOps: LanguageTypeOps, applyNotEquivalentTransformations: Boolean, ): MethodFormulaCubeCompact? { - var solver = MethodFormulaSolver(metaVarInfo, typeOps, applyNotEquivalentTransformations) + var solver = MethodFormulaSolver( + metaVarInfo, typeOps, applyNotEquivalentTransformations, + ) cube.positiveLiterals.forEach { solver = solver.addPositivePredicate(predicate(it)) @@ -313,7 +314,6 @@ fun MethodFormulaManager.simplifyMethodFormulaCube( } private class MethodConstraintsSolver { - private val positiveMetaVars = hashMapOf>() private val positiveParams = hashMapOf>() private var positiveNumberOfArgs: NumberOfArgsConstraint? = null private val positiveMethodModifiers = hashSetOf() @@ -333,12 +333,23 @@ private class MethodConstraintsSolver { fun addPositive(constraint: MethodConstraint): Unit? { when (constraint) { is ParamConstraint -> { - positiveParams.getOrPut(constraint.position, ::hashSetOf).add(constraint.condition) - - if (constraint.condition is IsMetavar) { - positiveMetaVars.getOrPut(constraint.position, ::hashSetOf) - .addAll(constraint.condition.metavar.basics) + val posSet = positiveParams.getOrPut(constraint.position, ::hashSetOf) + val cond = constraint.condition + if (cond is IsMetavar) { + // A (base, star=false) implies A* (base OR any-field, star=true), so A subsumes + // A* at the same position: `A ^ A*` == A. Keep only the base literal. + if (!cond.star) { + posSet.removeAll { + it is IsMetavar && it.star && it.metavar.basics.any { b -> b in cond.metavar.basics } + } + } else if (posSet.any { + it is IsMetavar && !it.star && it.metavar.basics.any { b -> b in cond.metavar.basics } + }) { + // A* is redundant when a coinciding base A is already required. + return Unit + } } + posSet.add(cond) } is NumberOfArgsConstraint -> { @@ -366,9 +377,27 @@ private class MethodConstraintsSolver { val currentPositive = positiveParams[constraint.position].orEmpty() if (constraint.condition in currentPositive) return null - if (constraint.condition is IsMetavar) { - val posMetaVars = positiveMetaVars[constraint.position].orEmpty() - if (constraint.condition.metavar.basics.any { it in posMetaVars }) return null + val negCond = constraint.condition + if (negCond is IsMetavar) { + // `$X` (star=false) is base/value taint = A; `$*X` (star=true) is base OR + // any-field = A*. They are DISTINCT literals related by the implication A => A*. + // Stars of the positive occurrences of the SAME metavar at this position: + val coincidingPositiveStars = currentPositive.asSequence() + .filterIsInstance() + .filter { pos -> pos.metavar.basics.any { it in negCond.metavar.basics } } + .map { it.star } + .toSet() + + // Contradiction (whole-match exclusion) per the A => A* truth table: + // !A* is UNSAT if A or A* is positive (A => A*); + // !A is UNSAT only if A (star=false) is positive -- if ONLY A* (star=true) + // is positive, `!A ^ A*` is SAT (field-only) and must be kept below. + val contradiction = if (negCond.star) { + coincidingPositiveStars.isNotEmpty() + } else { + false in coincidingPositiveStars + } + if (contradiction) return null } } @@ -410,7 +439,8 @@ private class MethodFormulaSolver( private val metaVarInfo: ResolvedMetaVarInfo, private val typeOps: LanguageTypeOps, private val applyNotEquivalentTransformations: Boolean, - private val positive: SolverConstraints = SolverConstraints(signature = null), + private val positive: SolverConstraints = + SolverConstraints(signature = null, constraints = MethodConstraintsSolver()), private val negated: MutableMap> = hashMapOf() ) { fun addPositivePredicate(predicate: Predicate): MethodFormulaSolver? { diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt index 4963111c9..c75419f1f 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt @@ -1,7 +1,9 @@ package org.opentaint.semgrep.pattern.conversion.taint +import org.opentaint.dataflow.configuration.TaintCleanReach import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase 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.SerializedCondition.Companion.mkFalse import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFunctionNameMatcher @@ -13,6 +15,13 @@ import org.opentaint.semgrep.pattern.Mark.GeneratedMark fun PositionBase.base(): PositionBaseWithModifiers = PositionBaseWithModifiers.BaseOnly(this) +fun PositionBaseWithModifiers.withAnyField(): PositionBaseWithModifiers = when (this) { + is PositionBaseWithModifiers.BaseOnly -> + PositionBaseWithModifiers.WithModifiers(base, listOf(PositionModifier.AnyField)) + is PositionBaseWithModifiers.WithModifiers -> + PositionBaseWithModifiers.WithModifiers(base, modifiers + PositionModifier.AnyField) +} + fun anyName() = SerializedSimpleNameMatcher.Pattern(".*") fun anyFunction() = SerializedFunctionNameMatcher.Complex(anyName(), anyName(), anyName()) @@ -45,8 +54,15 @@ fun serializedConditionOr(args: List): SerializedCondition fun GeneratedMark.mkContainsMark(pos: PositionBaseWithModifiers) = SerializedCondition.ContainsMark(taintMarkStr(), pos) +fun GeneratedMark.mkContainsMarkOnAnyField(pos: PositionBaseWithModifiers) = + SerializedCondition.ContainsMarkOnAnyField(taintMarkStr(), pos) + fun GeneratedMark.mkAssignMark(pos: PositionBaseWithModifiers) = SerializedTaintAssignAction(taintMarkStr(), pos = pos) fun GeneratedMark.mkCleanMark(pos: PositionBaseWithModifiers) = - SerializedTaintCleanAction(taintMarkStr(), pos = pos) + SerializedTaintCleanAction( + taintMarkStr(), + pos = pos, + reach = TaintCleanReach.ExactAndAnyField, + ) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt index f1a250015..9181fc4a2 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt @@ -473,7 +473,7 @@ private fun MethodConstraint.replaceMetavar(replace: (MetavarAtom) -> MetavarAto } val newCondition = when (condition) { - is IsMetavar -> IsMetavar(replace(condition.metavar) ?: return null) + is IsMetavar -> IsMetavar(replace(condition.metavar) ?: return null, condition.star) is ParamCondition.StringValueMetaVar -> ParamCondition.StringValueMetaVar( replace(condition.metaVar) ?: return null ) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintMarkCheckBuilder.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintMarkCheckBuilder.kt index 3adf7cc16..3b4511cd8 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintMarkCheckBuilder.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintMarkCheckBuilder.kt @@ -7,6 +7,7 @@ import org.opentaint.semgrep.pattern.Mark.GeneratedMark interface MarkConditionBuilder { fun checkTaintMark(mark: GeneratedMark, pos: PositionBaseWithModifiers): C + fun checkTaintMarkOnAnyField(mark: GeneratedMark, pos: PositionBaseWithModifiers): C fun negate(cond: C): C fun and(args: List): C fun or(args: List): C @@ -14,8 +15,20 @@ interface MarkConditionBuilder { fun mkFalse(): C } +// Lifts a plain [MarkConditionBuilder] into its any-field form: the mark leaf ([checkTaintMark]) +// is rerouted to [checkTaintMarkOnAnyField], while boolean structure (negate/and/or/true/false) is +// delegated unchanged. This is the single source of the plain -> any-field mapping, guaranteeing the +// plain and any-field arms carry the SAME mark on the SAME base, differing only ContainsMark vs +// ContainsMarkOnAnyField. Impls are stateless singletons, so delegation via `by inner` is sound. +class AnyFieldLift(private val inner: MarkConditionBuilder) : MarkConditionBuilder by inner { + override fun checkTaintMark(mark: GeneratedMark, pos: PositionBaseWithModifiers): C = + inner.checkTaintMarkOnAnyField(mark, pos) +} + data object JavaMarkConditionBuilder : MarkConditionBuilder { override fun checkTaintMark(mark: GeneratedMark, pos: PositionBaseWithModifiers) = mark.mkContainsMark(pos) + override fun checkTaintMarkOnAnyField(mark: GeneratedMark, pos: PositionBaseWithModifiers) = + mark.mkContainsMarkOnAnyField(pos) override fun negate(cond: SerializedCondition) = SerializedCondition.not(cond) override fun and(args: List) = SerializedCondition.and(args) override fun or(args: List) = serializedConditionOr(args) @@ -25,6 +38,14 @@ data object JavaMarkConditionBuilder : MarkConditionBuilder sealed interface TaintMarkCheckBuilder { fun build(builder: MarkConditionBuilder, position: PositionBaseWithModifiers): C + + // Any-field lift of [build]: every mark leaf becomes a "contains on any field" check on the + // same mark and position; boolean structure is preserved. Used by starred ($*X) sinks so the + // any-field arm stays coherent with the composed requires marks (same mark, same base). + // Defined once by lifting the builder ([AnyFieldLift]) rather than mirroring [build] per case, + // so a subclass that overrides only [build] cannot desync its any-field arm. + fun buildOnAnyField(builder: MarkConditionBuilder, position: PositionBaseWithModifiers): C = + build(AnyFieldLift(builder), position) } data class TaintMarkLabelCheckBuilder(val label: GeneratedMark) : TaintMarkCheckBuilder { diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleGenerationCtx.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleGenerationCtx.kt index c13ae60c7..536e5137d 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleGenerationCtx.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleGenerationCtx.kt @@ -25,6 +25,15 @@ data class TaintRuleGenerationCtx( pos: PositionBaseWithModifiers ): Cond? = null + // Any-field variant of [stateContains] for starred ($*X) sinks. Must emit the SAME mark(s) + // as [stateContains] on the SAME position, lifted to "contains on any field", so the plain + // and any-field arms of a starred sink stay coherent. Default mirrors stateContains == null. + fun stateContainsOnAnyField( + state: TaintRegisterStateAutomata.State, + varName: MetavarAtom, + pos: PositionBaseWithModifiers + ): Cond? = null + fun stateAssign( state: TaintRegisterStateAutomata.State, varName: MetavarAtom, @@ -143,6 +152,19 @@ data class TaintRuleGenerationCtx( return taintRuleStrategy.posContainsAnyMark(position, setOf(markName)) } + fun containsStateMarkOnAnyField( + varName: MetavarAtom, + state: TaintRegisterStateAutomata.State, + position: PositionBaseWithModifiers + ): Cond { + compositionStrategy?.stateContainsOnAnyField(state, varName, position)?.let { return it } + + val markName = stateMarkName(varName, state) + ?: return taintRuleStrategy.conditionBuilder.mkFalse() + + return taintRuleStrategy.conditionBuilder.checkTaintMarkOnAnyField(markName, position) + } + private fun usedTaintMarks(state: TaintRegisterStateAutomata.State): Set = state.register.assignedVars.keys.flatMapTo(hashSetOf()) { mv -> compositionStrategy?.stateAccessedMarks(state, mv)?.let { return@flatMapTo it } @@ -237,6 +259,16 @@ data class TaintRuleGenerationCtx( return taintRuleStrategy.conditionBuilder.or(conditions) } + fun containsMarkOnAnyFieldWithAnyStateBefore( + state: TaintRegisterStateAutomata.State, + varName: MetavarAtom, + position: PositionBaseWithModifiers + ): Cond { + val varStates = metaVarRelevantStates(state, varName) + val conditions = varStates.map { containsStateMarkOnAnyField(varName, it, position) } + return taintRuleStrategy.conditionBuilder.or(conditions) + } + private fun stateMarkName(varName: MetavarAtom, state: TaintRegisterStateAutomata.State): Mark.GeneratedMark? = state.register.assignedVars[varName]?.let { stateMarkName(varName, it) } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt index 00bab88b8..98c1b956d 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt @@ -506,6 +506,15 @@ private fun forkState( return newState } +// True when this edge assigns a whole-object (`$*X`) metavar. Used to propagate the star onto the +// synthesized `generated_source` mark for focus-free source patterns (e.g. `$*X = src()`), so the +// generated source taints the value AND all of its nested fields, matching the starred intent. +private fun EdgeEffect.assignsStarredMetaVar(): Boolean = + assignMetaVar.values.asSequence().flatten().any { + val constraint = it.predicate.constraint + constraint is ParamConstraint && (constraint.condition as? IsMetavar)?.star == true + } + private fun ensureSourceStateVars( automata: TaintRegisterStateAutomata, focusMetaVars: Set @@ -529,7 +538,10 @@ private fun ensureSourceStateVars( val effectVars = edge.effect.assignMetaVar.toMutableMap() // todo: currently we taint only result, but semgrep taint all subexpr by default - val condition = ParamConstraint(Position.Result, IsMetavar(freshVar)) + val condition = ParamConstraint( + Position.Result, + IsMetavar(freshVar, star = edge.effect.assignsStarredMetaVar()) + ) val predicate = Predicate(positivePredicate.signature, condition) effectVars[freshVar] = listOf(MethodPredicate(predicate, negated = false)) val effect = EdgeEffect(effectVars) @@ -544,7 +556,7 @@ private fun ensureSourceStateVars( val condition = ParamConstraint( Position.Argument(Position.ArgumentIndex.Any("tainted")), - IsMetavar(freshVar) + IsMetavar(freshVar, star = edge.effect.assignsStarredMetaVar()) ) val predicate = Predicate(positivePredicate.signature, condition) effectVars[freshVar] = listOf(MethodPredicate(predicate, negated = false)) @@ -560,7 +572,7 @@ private fun ensureSourceStateVars( val condition = ParamConstraint( Position.Argument(Position.ArgumentIndex.Concrete(idx = 0)), - IsMetavar(freshVar) + IsMetavar(freshVar, star = edge.effect.assignsStarredMetaVar()) ) val predicate = Predicate(positivePredicate.signature, condition) effectVars[freshVar] = listOf(MethodPredicate(predicate, negated = false)) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/JoinRightCompositionStrategy.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/JoinRightCompositionStrategy.kt index 75aac94ab..ba506502c 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/JoinRightCompositionStrategy.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/JoinRightCompositionStrategy.kt @@ -31,6 +31,19 @@ class JoinRightCompositionStrategy( return strategy.posContainsAnyMark(pos, leftFinalMarks) } + override fun stateContainsOnAnyField( + state: TaintRegisterStateAutomata.State, + varName: MetavarAtom, + pos: PositionBaseWithModifiers + ): Cond? { + if (varName != initialVar) return null + val value = state.register.assignedVars[varName] + if (value != initialStateId) return null + + val builder = strategy.conditionBuilder + return builder.or(leftFinalMarks.map { builder.checkTaintMarkOnAnyField(it, pos) }) + } + override fun stateAccessedMarks( state: TaintRegisterStateAutomata.State, varName: MetavarAtom diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt index 48bdfbdc2..510b9342a 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt @@ -2,6 +2,7 @@ package org.opentaint.semgrep.pattern.conversion.taint.composition import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier import org.opentaint.semgrep.pattern.Mark import org.opentaint.semgrep.pattern.conversion.MetavarAtom import org.opentaint.semgrep.pattern.conversion.TaintRuleStrategy @@ -9,6 +10,7 @@ import org.opentaint.semgrep.pattern.conversion.taint.TaintAutomataEdges import org.opentaint.semgrep.pattern.conversion.taint.TaintRegisterStateAutomata import org.opentaint.semgrep.pattern.conversion.taint.TaintRuleGenerationCtx import org.opentaint.semgrep.pattern.conversion.taint.base +import org.opentaint.semgrep.pattern.conversion.taint.withAnyField class TaintCleanCompositionStrategy( private val rule: TaintAutomataEdges, @@ -30,7 +32,22 @@ class TaintCleanCompositionStrategy( cleanerPos += PositionBase.This.base() } - return cleans.flatMap { c -> cleanerPos.map { strategy.createCleanAction(c, it) } } + val isStar = pos is PositionBaseWithModifiers.WithModifiers && + pos.modifiers.contains(PositionModifier.AnyField) + // star ($*X): clean the any-field of each cleaner position (Result.*, etc.), + // on the SAME base as the plain value clean — not the raw metavar position. + val cleanerEmitPositions = if (isStar) cleanerPos.map { it.withAnyField() } else cleanerPos + + // Also clean the focus metavar's own position (`pos`, e.g. the sanitized argument). A + // pass-through sanitizer (`clean($C) { return $C }`) carries the argument's taint into its + // result, but the clean runs on the argument-keyed fact at call-to-start, where the Result + // position does not yet exist — cleaning only Result misses it. Cleaning the focus position + // removes the taint on the flow entering the call; it is flow-specific, so a separate use of + // the same variable outside this call stays tainted. For a star clean `pos` already carries + // the AnyField modifier, so this stays coherent with the plain-value arm's base. + val emitPositions = (cleanerEmitPositions + listOfNotNull(pos)).distinct() + + return cleans.flatMap { c -> emitPositions.map { strategy.createCleanAction(c, it) } } } override fun stateAccessedMarks( diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintPassCompositionStrategy.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintPassCompositionStrategy.kt index 5d8cd9602..8628d126f 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintPassCompositionStrategy.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintPassCompositionStrategy.kt @@ -28,6 +28,16 @@ class TaintPassCompositionStrategy( return markRequires.build(strategy.conditionBuilder, pos) } + override fun stateContainsOnAnyField( + state: TaintRegisterStateAutomata.State, + varName: MetavarAtom, + pos: PositionBaseWithModifiers + ): Cond? { + val value = state.register.assignedVars[varName] + if (value != initialStateId) return null + return markRequires.buildOnAnyField(strategy.conditionBuilder, pos) + } + override fun stateAssign( state: TaintRegisterStateAutomata.State, varName: MetavarAtom, diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSinkCompositionStrategy.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSinkCompositionStrategy.kt index 0c50cd6e0..cd2bed6a3 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSinkCompositionStrategy.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSinkCompositionStrategy.kt @@ -27,6 +27,16 @@ class TaintSinkCompositionStrategy( return requires.build(strategy.conditionBuilder, pos) } + override fun stateContainsOnAnyField( + state: TaintRegisterStateAutomata.State, + varName: MetavarAtom, + pos: PositionBaseWithModifiers + ): Cond? { + val value = state.register.assignedVars[varName] + if (value != initialStateId) return null + return requires.buildOnAnyField(strategy.conditionBuilder, pos) + } + override fun stateAccessedMarks( state: TaintRegisterStateAutomata.State, varName: MetavarAtom diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSourceCompositionStrategy.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSourceCompositionStrategy.kt index 12e460189..c01d5899c 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSourceCompositionStrategy.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintSourceCompositionStrategy.kt @@ -32,6 +32,19 @@ class TaintSourceCompositionStrategy( return requires.build(strategy.conditionBuilder, pos) } + override fun stateContainsOnAnyField( + state: TaintRegisterStateAutomata.State, + varName: MetavarAtom, + pos: PositionBaseWithModifiers + ): Cond? { + if (requires == null) return null + + val value = state.register.assignedVars[varName] + if (value != initialStateId) return null + + return requires.buildOnAnyField(strategy.conditionBuilder, pos) + } + override fun stateAssign( state: TaintRegisterStateAutomata.State, varName: MetavarAtom, diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/SerializedConditionRoundTripTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/SerializedConditionRoundTripTest.kt new file mode 100644 index 000000000..8dc728880 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/SerializedConditionRoundTripTest.kt @@ -0,0 +1,59 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.ConfigurationLoader +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase +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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Guards the $*VAR star-operator serialization footgun: ContainsMarkOnAnyField is + * field-shape-identical to ContainsMark, so before it got a distinct discriminating + * key a YAML round-trip silently demoted it to plain ContainsMark, losing the + * any-field/field-sensitive semantics. + */ +class SerializedConditionRoundTripTest { + private val yaml = ConfigurationLoader.yaml + + private val pos = PositionBaseWithModifiers.WithModifiers( + base = PositionBase.Argument(0), + modifiers = listOf(PositionModifier.AnyField), + ) + + @Test + fun `ContainsMarkOnAnyField survives a YAML round-trip`() { + val original: SerializedCondition = SerializedCondition.ContainsMarkOnAnyField( + tainted = "untrusted", + pos = pos, + ) + + val encoded = yaml.encodeToString(SerializedCondition.serializer(), original) + val decoded = yaml.decodeFromString(SerializedCondition.serializer(), encoded) + + assertTrue( + decoded is SerializedCondition.ContainsMarkOnAnyField, + "round-trip must preserve ContainsMarkOnAnyField, got ${decoded::class.simpleName}; yaml=\n$encoded", + ) + assertEquals(original, decoded) + } + + @Test + fun `ContainsMark still round-trips as ContainsMark`() { + val original: SerializedCondition = SerializedCondition.ContainsMark( + tainted = "untrusted", + pos = pos, + ) + + val encoded = yaml.encodeToString(SerializedCondition.serializer(), original) + val decoded = yaml.decodeFromString(SerializedCondition.serializer(), encoded) + + assertTrue( + decoded is SerializedCondition.ContainsMark, + "round-trip must preserve ContainsMark, got ${decoded::class.simpleName}; yaml=\n$encoded", + ) + assertEquals(original, decoded) + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorParseTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorParseTest.kt new file mode 100644 index 000000000..49d25e116 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorParseTest.kt @@ -0,0 +1,132 @@ +package org.opentaint.semgrep + +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.tree.ParseTree +import org.opentaint.semgrep.pattern.Metavar +import org.opentaint.semgrep.pattern.SemgrepJavaPattern +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TypedMetavar +import org.opentaint.semgrep.pattern.antlr.JavaLexer +import org.opentaint.semgrep.pattern.antlr.JavaParser +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.errorEntries +import org.opentaint.semgrep.pattern.parseJavaSemgrepPattern +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class StarOperatorParseTest { + private fun collect(p: SemgrepJavaPattern): List = + listOf(p) + p.children.flatMap { collect(it) } + + private fun metavars(pattern: String): List = + collect(parseJavaSemgrepPattern(pattern)).filterIsInstance() + + private fun blockingErrors(ruleText: String): List { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("repro.yaml"), Path("."), trace) + loader.loadRules() + return trace.errorEntries().map { "${it.severity}/${it.step}: ${it.message}" } + } + + // Walks the raw ANTLR parse tree (the parser path the rule loader uses) and counts how many + // starred-metavar alternatives ($*VAR) were recognized in expression position. + private fun starredMetavarCount(pattern: String): Int { + val lexer = JavaLexer(CharStreams.fromString(pattern)) + val parser = JavaParser(CommonTokenStream(lexer)) + val tree: ParseTree = parser.semgrepPattern() + var count = 0 + fun walk(node: ParseTree) { + if (node is JavaParser.PrimaryStarredMetavarContext) count++ + for (i in 0 until node.childCount) walk(node.getChild(i)) + } + walk(tree) + return count + } + + @Test + fun `starred metavar in call argument`() { + val mvs = metavars("sink(\$*Y);") + val y = mvs.single { it.name == "\$Y" } + assertTrue(y.star, "expected \$*Y to be starred") + } + + @Test + fun `prefix star, not suffix, marks the metavar`() { + // The star is a `$*` prefix bound into the metavar token. `$Y * z` stays multiplication, + // and the retired suffix form `$Y*` is no longer a starred metavar. + assertEquals(1, starredMetavarCount("sink(\$*Y);"), "\$*Y must be a star") + assertEquals(0, starredMetavarCount("sink(\$Y * z);"), "\$Y * z must not be a star") + assertEquals(0, starredMetavarCount("sink(\$Y*);"), "retired suffix \$Y* must not be a star") + } + + @Test + fun `starred formal parameter metavar`() { + val mvs = metavars( + "@\$ANNOTATION(...) \$RT \$M(..., \$TYPE \$*UNTRUSTED, ...) { ... }" + ) + val u = mvs.single { it.name == "\$UNTRUSTED" } + assertTrue(u.star, "expected formal-parameter \$*UNTRUSTED to be starred") + } + + @Test + fun `starred typed variable declaration`() { + // F5: the starred `variableDeclaratorId` alternative in a TYPED declaration must load + // (no parse exception) and the declared LHS metavar must carry star=true. + val mvs = metavars("String \$*UNTRUSTED = \$REQ.getParameter(\"q\");") + val u = mvs.single { it.name == "\$UNTRUSTED" } + assertTrue(u.star, "expected typed-declaration \$*UNTRUSTED to be starred") + } + + @Test + fun `starred typed metavar in receiver position`() { + // `(Type $*VAR).m()` is how a sink observes taint buried in a field of its RECEIVER — + // e.g. a java.io.File whose path is tainted but whose base carries no mark. Before the + // typedVariableExpression grammar accepted STARRED_METAVAR this failed to parse, and the + // whole pattern was dropped SILENTLY (the rule count shrank, no error was reported). + val typed = collect(parseJavaSemgrepPattern("(java.io.File \$*FILE).exists();")) + .filterIsInstance() + val f = typed.single { it.name == "\$FILE" } + assertTrue(f.star, "expected receiver \$*FILE to be starred") + } + + @Test + fun `starred typed metavar in argument position`() { + val typed = collect(parseJavaSemgrepPattern("sink((java.nio.file.Path \$*P));")) + .filterIsInstance() + val p = typed.single { it.name == "\$P" } + assertTrue(p.star, "expected parenthesised typed \$*P to be starred") + } + + @Test + fun `starred bare return value`() { + val mvs = metavars("return \$*UNTRUSTED;") + val u = mvs.single { it.name == "\$UNTRUSTED" } + assertTrue(u.star) + } + + @Test + fun `star pattern loads without blocking errors and binds same name`() { + // \$UNTRUSTED appears starred in the source and unstarred in the sink; + // they must refer to the same metavariable and the rule must load cleanly. + val rule = """ + rules: + - id: star-bind-repro + options: { lib: true } + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}*UNTRUSTED = src(); + pattern-sinks: + - pattern: sink(${'$'}UNTRUSTED); + """.trimIndent() + val errors = blockingErrors(rule) + assertTrue(errors.isEmpty(), "star rule failed to load:\n" + errors.joinToString("\n")) + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorRuleGenTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorRuleGenTest.kt new file mode 100644 index 000000000..93477d9e8 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorRuleGenTest.kt @@ -0,0 +1,352 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition +import org.opentaint.dataflow.configuration.jvm.serialized.SinkRule +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.createTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.configuration.jvm.serialized.SourceRule +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +class StarOperatorRuleGenTest { + protected fun config(ruleText: String): SerializedTaintConfig { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), trace) + val (rule, _) = loader.loadRules().rulesWithMeta.single() + @Suppress("UNCHECKED_CAST") + return (rule as TaintRuleFromSemgrep).createTaintConfig() + } + + private fun allConditions(cfg: SerializedTaintConfig): List { + val sinkRules: List = buildList { + addAll(cfg.sink.orEmpty()) + addAll(cfg.methodExitSink.orEmpty()) + addAll(cfg.methodEntrySink.orEmpty()) + } + val sourceRules: List = buildList { + addAll(cfg.source.orEmpty()) + addAll(cfg.methodExitSource.orEmpty()) + addAll(cfg.entryPoint.orEmpty()) + } + val conditions = sinkRules.mapNotNull { it.condition } + + sourceRules.mapNotNull { it.condition } + + cfg.passThrough.orEmpty().mapNotNull { it.condition } + return conditions.flatMap { flatten(it) } + } + + private fun flatten(c: SerializedCondition): List = when (c) { + is SerializedCondition.Or -> listOf(c) + c.anyOf.flatMap { flatten(it) } + is SerializedCondition.And -> listOf(c) + c.allOf.flatMap { flatten(it) } + else -> listOf(c) + } + + private fun sourceAssignPositions(cfg: SerializedTaintConfig): List = + (cfg.source.orEmpty() + cfg.entryPoint.orEmpty()).filterIsInstance() + .flatMap { it.taint } + .map { it.pos } + + private fun cleanPositions(cfg: SerializedTaintConfig): List = + cfg.cleaner.orEmpty() + .flatMap { it.cleans } + .map { it.pos } + + @Test + fun `starred source assigns value and any-field`() { + val cfg = config( + """ + rules: + - id: star-source + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - patterns: + - pattern: sink(${'$'}*X); + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: other(${'$'}Y); + """.trimIndent() + ) + val positions = sourceAssignPositions(cfg) + assertTrue( + positions.any { it is PositionBaseWithModifiers.BaseOnly }, + "expected a plain-value assign; got $positions" + ) + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field assign; got $positions" + ) + } + + @Test + fun `starred assignment-LHS source assigns value and any-field`() { + // F1: the star sits on the assignment LHS metavar (`$*X = src()`), not a call argument. + // The whole-object taint must still emit BOTH a plain-value assign and an any-field assign. + val cfg = config( + """ + rules: + - id: star-assign-source + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}*X = src(); + pattern-sinks: + - pattern: sink(${'$'}Y); + """.trimIndent() + ) + val positions = sourceAssignPositions(cfg) + assertTrue( + positions.any { it is PositionBaseWithModifiers.BaseOnly }, + "expected a plain-value assign; got $positions" + ) + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field assign; got $positions" + ) + } + + @Test + fun `starred typed-declaration source assigns value and any-field`() { + // F5: a starred TYPED declaration (`String $*X = src()`) must load and thread the star + // through the assignment path, emitting both a plain-value and an any-field assign. + val cfg = config( + """ + rules: + - id: star-typed-decl-source + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: String ${'$'}*X = src(); + pattern-sinks: + - pattern: sink(${'$'}Y); + """.trimIndent() + ) + val positions = sourceAssignPositions(cfg) + assertTrue( + positions.any { it is PositionBaseWithModifiers.BaseOnly }, + "expected a plain-value assign; got $positions" + ) + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field assign; got $positions" + ) + } + + @Test + fun `starred sanitizer cleans value and any-field`() { + val cfg = config( + """ + rules: + - id: star-sanitizer + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sanitizers: + - patterns: + - pattern: clean(${'$'}*X); + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: sink(${'$'}X); + """.trimIndent() + ) + val positions = cleanPositions(cfg) + val plain = positions.filterIsInstance() + val anyField = positions.filterIsInstance() + .filter { it.modifiers.contains(PositionModifier.AnyField) } + assertTrue(plain.isNotEmpty(), "expected a plain-value clean; got $positions") + assertTrue(anyField.isNotEmpty(), "expected an any-field clean; got $positions") + // Base coherence: the any-field clean must sit on the SAME base as the + // plain value clean (both PositionBase.Result), not the raw metavar + // argument position — otherwise field taint survives the sanitizer. + assertTrue( + plain.any { it.base == PositionBase.Result }, + "expected plain clean on Result; got $positions" + ) + assertTrue( + anyField.any { it.base == PositionBase.Result }, + "expected any-field clean on Result (same base as plain value clean); got $positions" + ) + } + + @Test + fun `starred sanitizer assignment cleans returned value and any-field`() { + val cfg = config( + """ + rules: + - id: starred-receiver-sanitizer + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sanitizers: + - patterns: + - pattern: ${'$'}*CLEAN = ${'$'}REQ.clean(); + - focus-metavariable: ${'$'}CLEAN + pattern-sinks: + - pattern: sink(${'$'}X); + """.trimIndent() + ) + val positions = cleanPositions(cfg) + + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.base == PositionBase.Result && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field clean on Result; got $positions" + ) + } + + @Test + fun `starred sink produces ContainsMarkOnAnyField`() { + val cfg = config( + """ + rules: + - id: star-sink + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}*Y); + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = allConditions(cfg) + val anyField = conditions.filterIsInstance() + assertTrue(anyField.isNotEmpty(), "expected a ContainsMarkOnAnyField in the starred sink config") + + // Base coherence: every any-field check must be paired with a plain + // ContainsMark on the SAME mark and SAME position base — a starred sink + // matches the value OR any of its nested fields, both anchored to the + // metavar's resolved position. A base/mark mismatch would be a silent bug. + val plain = conditions.filterIsInstance() + anyField.forEach { af -> + assertTrue( + plain.any { it.tainted == af.tainted && it.pos.base == af.pos.base }, + "any-field check $af has no paired plain ContainsMark on the same mark/base; plain=$plain" + ) + } + } + + @Test + fun `starred propagator copies over any-field`() { + val cfg = config( + """ + rules: + - id: star-prop + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-propagators: + - patterns: + - pattern: ${'$'}TO = wrap(${'$'}*X); + from: ${'$'}X + to: ${'$'}TO + pattern-sinks: + - pattern: sink(${'$'}TO); + """.trimIndent() + ) + // A starred propagator source occurrence must reference an any-field position + val anyField = allConditions(cfg).any { it is SerializedCondition.ContainsMarkOnAnyField } || + sourceAssignPositions(cfg).any { + it is PositionBaseWithModifiers.WithModifiers && it.modifiers.contains(PositionModifier.AnyField) + } + assertTrue(anyField, "expected any-field involvement in starred propagator") + } + + @Test + fun `starred pattern-not sink still generates any-field check`() { + val cfg = config( + """ + rules: + - id: star-not + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}*Y); + - pattern-not: sink(safe()); + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + assertTrue( + allConditions(cfg).any { it is SerializedCondition.ContainsMarkOnAnyField }, + "pattern-not sink must still carry the any-field check" + ) + } + + // Mirrors the shipped xss sanitizer shape: the starred metavar sits inside a `pattern-either` + // at a NON-first arg position with leading/trailing varargs `...`, focused separately. This is + // the shape the OWASP escapeHtml sanitizer uses; if the any-field clean is lost here (but not + // in the simple `clean($*X)` case), that explains why starring the shipped sanitizer was a no-op. + @Test + fun `starred sanitizer in pattern-either with varargs still cleans any-field on Result`() { + val cfg = config( + """ + rules: + - id: star-san-either + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: esc(..., ${'$'}*X, ...); + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: sink(${'$'}X); + """.trimIndent() + ) + val positions = cleanPositions(cfg) + val anyField = positions.filterIsInstance() + .filter { it.modifiers.contains(PositionModifier.AnyField) } + assertTrue( + anyField.any { it.base == PositionBase.Result }, + "expected any-field clean on Result for the pattern-either/varargs sanitizer; got $positions" + ) + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorTest.kt new file mode 100644 index 000000000..3af1d4319 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorTest.kt @@ -0,0 +1,118 @@ +package org.opentaint.semgrep + +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.opentaint.semgrep.util.SampleBasedTest +import org.opentaint.semgrep.util.TestAnalysisRunner +import kotlin.test.Test + +@TestInstance(PER_CLASS) +class StarOperatorTest : SampleBasedTest() { + // The starred SOURCE ($*X = src()) taints the whole object and every field; a concrete + // field read only inherits that taint once the any-accessor is unrolled to a field read. + // Mirror the Go harness and enable unrolling for THIS sample only (StarSink/StarSanitizer + // keep the default AnyAccessorDisabled). Removing the source `*` makes the Positive a false + // negative, proving the star is load-bearing here. + @Test + fun `star source field flow`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star sink any field`() = runTest() + + @Test + fun `star sanitizer clears field taint`() = runTest() + + // ---- Deep-nesting matrix: taint hidden 5+ fields deep and/or 5+ calls deep ---- + + // Starred source, taint 5 fields deep, unhidden by a nested field read. Needs the + // any-accessor unroll (like `star source field flow`) so the source star reaches a + // concrete deep field read. + @Test + fun `star deep source field flow`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Starred sink observes taint written 5 fields deep (default unroll). + @Test + fun `star deep sink any field`() = runTest() + + // Starred sanitizer must clear taint 5 fields deep (default unroll). + @Test + fun `star deep sanitizer clears field taint`() = runTest() + + // Starred source threaded through a 5+ hop interprocedural chain that alternately hides + // taint inside an object and exposes it. Needs the any-accessor unroll. + @Test + fun `star interprocedural chain`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Both ends starred: whole-object source + whole-object sink, nested object in between. + @Test + fun `star source and sink`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Starred source + starred sanitizer over a deep field chain. + @Test + fun `star source and sanitizer`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // ---- Combined matrix: 5+ interprocedural depth x 5+ field depth, sources/sinks deep ---- + // + // Every StarMatrix* sample places the source statement 5 calls deep, the sink call 5 calls + // deep, and moves the taint one field level per hop (or threads a 5-level object), so the + // interprocedural and field dimensions are exercised TOGETHER, not separately. + + @Test + fun `star matrix source - deep source, per-hop unwrap, deep sink`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star matrix sink - deep source, per-hop wrap, deep starred sink`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Both propagator occurrences starred ($*T = pass($*F)): the FROM observes any-field taint + // of the whole argument, the TO assigns whole-object taint verified by a per-hop unwrap. + @Test + fun `star matrix propagator - starred from and to move whole-object taint`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // The starred clean sits inside a wrapper helper — the deep-mark-exclusion regression shape. + @Test + fun `star matrix sanitizer - wrapped whole-object clean across summaries`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Composition stress for the deep mark exclusions: the starred clean under TWO wrapper + // summaries, with the sanitized flow itself inside a further summarized helper. + @Test + fun `star nested wrapper sanitizer - deep exclusion composes across summary levels`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Mixed exclusion kinds on one flow: deep (starred clean) + plain (value clean) refinements + // of the same initial fact — the always-propagate-deep-marks regression net. + @Test + fun `star mixed exclusion sanitizer - deep and plain exclusions compose`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star matrix pattern-not - starred sink with excluded emit mode`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star matrix pattern-inside - starred sink gated by receiver origin`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // The pattern-inside context that wires the guard receiver ($G = checker(); ...) converts + // via the state-var mechanism, like the shipped setContentType suppression. + @Test + fun `star matrix pattern-not-inside - starred sink suppressed by guard`() = + runTest( + expectStateVar = true, + unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled, + ) + + @AfterAll + fun close() { + closeRunner() + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotCoincidenceTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotCoincidenceTest.kt new file mode 100644 index 000000000..e0c302d93 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotCoincidenceTest.kt @@ -0,0 +1,92 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.createTaintConfig +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * The star / pattern-not coincidence matrix on a METHOD-DECLARATION source (a `pattern-not` that + * negates the same parameter position). This is a distinct code path from the call-argument shape in + * [StarPatternNotFieldOnlyTest]: a method-declaration `pattern-not` reaches + * `MethodConstraintsSolver.addNegative`, whereas a call-argument `pattern-not` collapses at an + * earlier automata-transform phase. + * + * With `$X` (base, A) and `$*X` (whole-object, A*) kept DISTINCT under the implication `A => A*`: + * - T/F (`$*X` positive ^ `pattern-not $X`) => `!A ^ A*` = field-only (keep field, drop base). + * - T/T (`$*X` positive ^ `pattern-not $*X`) => `!A* ^ A*` = contradiction = exclude-all. + * The two must produce DIFFERENT configs. + */ +class StarPatternNotCoincidenceTest { + private fun loadConfig(ruleText: String): SerializedTaintConfig? { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), trace) + val ruleWithMeta = loader.loadRules().rulesWithMeta.singleOrNull() + @Suppress("UNCHECKED_CAST") + return (ruleWithMeta?.first as? TaintRuleFromSemgrep)?.createTaintConfig() + } + + /** + * A method-declaration source whose `pattern-not` negates the same `$UNTRUSTED` parameter. + * @param positiveStar star on the positive `$UNTRUSTED` occurrence + * @param notMetavar the metavar spelled in the `pattern-not` param slot (with or without `*`) + */ + private fun methodRule(id: String, positiveStar: Boolean, notMetavar: String): String { + val pos = if (positiveStar) "${'$'}*UNTRUSTED" else "${'$'}UNTRUSTED" + return """ + rules: + - id: $id + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - patterns: + - pattern: | + @${'$'}ANNOTATION(...) + ${'$'}RETURNTYPE ${'$'}METHODNAME(..., ${'$'}TYPE $pos,...) { + ... + } + - pattern-not: | + @${'$'}ANNOTATION(...) + ${'$'}RETURNTYPE ${'$'}METHODNAME(..., @PathVariable ${'$'}TYPE $notMetavar,...) { + ... + } + pattern-sinks: + - pattern: sink(${'$'}S); + """.trimIndent() + } + + @Test + fun `T-F is a supported field-only exclusion and loads`() { + // `$*UNTRUSTED` positive (A*) + `pattern-not $UNTRUSTED` (base A) => `!A ^ A*` = field-only. + val config = loadConfig(methodRule("cmp", positiveStar = true, notMetavar = "${'$'}UNTRUSTED")) + assertTrue(config != null, "field-only T/F rule must load") + } + + @Test + fun `T-F is field-only, distinct from the full-exclusion T-T case`() { + // Same id so the two configs are comparable (marks embed the rule id). + val tf = loadConfig(methodRule("cmp", positiveStar = true, notMetavar = "${'$'}UNTRUSTED")) + val tt = loadConfig(methodRule("cmp", positiveStar = true, notMetavar = "${'$'}*UNTRUSTED")) + assertTrue(tf != null, "T/F (field-only) rule must load") + // T/T (`$*UNTRUSTED` ^ `pattern-not $*UNTRUSTED`) is a genuine contradiction => exclude-all, + // which drops the source, so its config differs from the field-only T/F config. + assertTrue(tf != tt, "T/F (field-only) must differ from T/T (exclude-all); tt=$tt") + } + + @Test + fun `structural non-coinciding pattern-not loads`() { + // The pattern-not negates the same position with a DIFFERENT metavar (`$OTHER`) — a genuine + // structural exclusion, not a coincidence with the positive `$*UNTRUSTED`. + val config = loadConfig(methodRule("structural", positiveStar = true, notMetavar = "${'$'}OTHER")) + assertTrue(config != null, "a non-coinciding structural pattern-not rule must load") + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotFieldOnlyTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotFieldOnlyTest.kt new file mode 100644 index 000000000..2a7c77862 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotFieldOnlyTest.kt @@ -0,0 +1,137 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SinkRule +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.createTaintConfig +import org.opentaint.semgrep.pattern.errorEntries +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * `pattern: sink($*X)` (whole-object / any-field) with a coinciding `pattern-not: sink($X)` + * (base/value) at the SAME position is a SCOPED exclusion: exclude the base value, keep the + * fields. With $X and $*X kept distinct and the implication A => A* encoded, that cell + * (`!A ^ A*`) is SAT and must compile to `ContainsMarkOnAnyField(pos) ^ !ContainsMark(pos)`. + */ +class StarPatternNotFieldOnlyTest { + private fun config(ruleText: String): SerializedTaintConfig { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), trace) + val rules = loader.loadRules().rulesWithMeta + val rule = rules.singleOrNull()?.first + ?: error("expected exactly 1 rule, got ${rules.size}; trace errors=${trace.errorEntries().map { it.message }}") + @Suppress("UNCHECKED_CAST") + return (rule as TaintRuleFromSemgrep).createTaintConfig() + } + + private fun flatten(c: SerializedCondition): List = when (c) { + is SerializedCondition.Or -> listOf(c) + c.anyOf.flatMap { flatten(it) } + is SerializedCondition.And -> listOf(c) + c.allOf.flatMap { flatten(it) } + is SerializedCondition.Not -> listOf(c) + flatten(c.not) + else -> listOf(c) + } + + private fun sinkConditions(cfg: SerializedTaintConfig): List = + (cfg.sink.orEmpty() + cfg.methodExitSink.orEmpty() + cfg.methodEntrySink.orEmpty()) + .filterIsInstance() + .mapNotNull { it.condition } + .flatMap { flatten(it) } + + private val rule = """ + rules: + - id: fo + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}*X); + - pattern-not: sink(${'$'}X); + - focus-metavariable: ${'$'}X + """.trimIndent() + + + private fun ruleCount(sinkPatterns: String): Int { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + val text = """ + rules: + - id: c + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: +$sinkPatterns + - focus-metavariable: ${'$'}X + """.trimIndent() + loader.registerRuleSet(text, Path("c.yaml"), Path("."), trace) + return loader.loadRules().rulesWithMeta.size + } + + @Test + fun `starred sink with coinciding unstarred pattern-not is field-only`() { + // !A ^ A* + val conds = sinkConditions(config(rule)) + assertTrue( + conds.any { it is SerializedCondition.ContainsMarkOnAnyField }, + "field-only sink must REQUIRE any-field taint (ContainsMarkOnAnyField); got $conds" + ) + assertTrue( + conds.any { it is SerializedCondition.Not && it.not is SerializedCondition.ContainsMark }, + "field-only sink must EXCLUDE base/value taint (Not(ContainsMark)); got $conds" + ) + } + + @Test + fun `A and A star both positive minimizes to A (base only, no any-field)`() { + // A ^ A* == A: base subsumes base-or-any-field. + val conds = sinkConditions(config(""" + rules: + - id: aa + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}X); + - pattern: sink(${'$'}*X); + - focus-metavariable: ${'$'}X + """.trimIndent())) + assertTrue( + conds.any { it is SerializedCondition.ContainsMark }, + "A ^ A* must keep the base ContainsMark; got $conds" + ) + assertTrue( + conds.none { it is SerializedCondition.ContainsMarkOnAnyField }, + "A ^ A* must drop the redundant any-field (solution is A); got $conds" + ) + } + + @Test + fun `base positive with starred pattern-not is unsatisfiable`() { + // A ^ !A* : A => A*, so excluding A* contradicts A -> UNSAT -> the sink variant is dropped. + val n = ruleCount( + " - pattern: sink(${'$'}X);\n" + + " - pattern-not: sink(${'$'}*X);" + ) + assertTrue(n == 0, "A ^ !A* must be unsatisfiable (rule dropped); got rules=$n") + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/CreateTaintConfig.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/CreateTaintConfig.kt new file mode 100644 index 000000000..1d9aab42b --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/CreateTaintConfig.kt @@ -0,0 +1,28 @@ +package org.opentaint.semgrep.pattern + +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFieldRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +/** + * Test helper: flattens a generated rule into a [SerializedTaintConfig] so the star-operator + * rule-generation tests can inspect the emitted source/sink/passThrough/cleaner items directly. + * + * The production analyzer no longer needs this (rules flow through the rule provider), so it lives + * in the test scope. + */ +fun TaintRuleFromSemgrep.createTaintConfig(): SerializedTaintConfig { + val rules = taintRules.flatMap { it.rules } + return SerializedTaintConfig( + entryPoint = rules.filterIsInstance(), + source = rules.filterIsInstance(), + methodExitSource = rules.filterIsInstance(), + sink = rules.filterIsInstance(), + passThrough = rules.filterIsInstance(), + cleaner = rules.filterIsInstance(), + methodExitSink = rules.filterIsInstance(), + methodEntrySink = rules.filterIsInstance(), + staticFieldSource = rules.filterIsInstance(), + ) +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtilsTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtilsTest.kt new file mode 100644 index 000000000..66cae210f --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtilsTest.kt @@ -0,0 +1,22 @@ +package org.opentaint.semgrep.pattern.conversion.taint + +import org.opentaint.dataflow.configuration.TaintCleanReach +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase +import org.opentaint.semgrep.pattern.Mark +import kotlin.test.Test +import kotlin.test.assertEquals + +class SerializedRuleUtilsTest { + @Test + fun `generated state cleanup reaches the AnyField fact alternative`() { + val mark = Mark.RuleUniqueMarkPrefix( + ruleId = "rule", + modeModifier = null, + idx = 0, + ).artificialState("state") + + val cleanup = mark.mkCleanMark(PositionBase.Result.base()) + + assertEquals(TaintCleanReach.ExactAndAnyField, cleanup.reach) + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt index 257088163..46eabecbf 100644 --- a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt @@ -1,6 +1,7 @@ package org.opentaint.semgrep.util import base.RuleSample +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig @@ -21,12 +22,14 @@ abstract class SampleBasedTest( ) { inline fun runTest( expectStateVar: Boolean = false, + unrollStrategy: AnyAccessorUnrollStrategy = AnyAccessorUnrollStrategy.AnyAccessorDisabled, noinline provideAdditionalRules: (SerializedTaintConfig) -> SerializedTaintConfig = { it } - ) = runClassTest(getFullyQualifiedClassName(), expectStateVar, provideAdditionalRules) + ) = runClassTest(getFullyQualifiedClassName(), expectStateVar, unrollStrategy, provideAdditionalRules) fun runClassTest( sampleClassName: String, expectStateVar: Boolean, + unrollStrategy: AnyAccessorUnrollStrategy, provideAdditionalRules: (SerializedTaintConfig) -> SerializedTaintConfig ) { val data = sampleData[sampleClassName] ?: error("No sample data for $sampleClassName") @@ -57,7 +60,7 @@ abstract class SampleBasedTest( val configWithExtraRules = provideAdditionalRules(SerializedTaintConfig()) - val results = runner.run(javaRule, configWithExtraRules, configurationRequired, allSamples) + val results = runner.run(javaRule, configWithExtraRules, configurationRequired, allSamples, unrollStrategy) val missedPositive = hashSetOf() for (sample in data.positiveClasses) { diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt index d49df73e5..b07b7f2e6 100644 --- a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt @@ -4,6 +4,9 @@ import kotlinx.coroutines.runBlocking 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.Accessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy import org.opentaint.dataflow.ap.ifds.access.ApMode import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace @@ -68,15 +71,19 @@ class TestAnalysisRunner( } @Suppress("UNCHECKED_CAST") - private fun setupEngine(configProvider: TaintRulesProvider): TaintAnalyzer { + private fun setupEngine( + configProvider: TaintRulesProvider, + unrollStrategy: AnyAccessorUnrollStrategy, + ): TaintAnalyzer { val options = TaintAnalyzerOptions( ifdsTimeout = 1.minutes, ifdsApMode = ApMode.Tree ) + val strategy = unrollStrategy val analyzer = object : TaintAnalyzer(options) { override val unrollStrategy: AnyAccessorUnrollStrategy - get() = AnyAccessorUnrollStrategy.AnyAccessorDisabled + get() = strategy override fun analysisGraph() = ifdsAnalysisGraph override fun analysisManager() = JIRAnalysisManager(cp, refManager, configProvider) @@ -97,7 +104,8 @@ class TestAnalysisRunner( rule: TaintRuleFromSemgrep, config: SerializedTaintConfig, useDefaultConfig: Boolean, - samples: Set + samples: Set, + unrollStrategy: AnyAccessorUnrollStrategy = AnyAccessorUnrollStrategy.AnyAccessorDisabled, ): Map> = samples.associate { sample -> val cls = cp.findClassOrNull(sample) ?: error("No sample in CP") @@ -105,7 +113,7 @@ class TestAnalysisRunner( ?: error("No entrypoint in $sample") val rulesProvider = rulesProvider(rule, config, useDefaultConfig) - setupEngine(rulesProvider).use { engine -> + setupEngine(rulesProvider, unrollStrategy).use { engine -> val traces = engine.analyzeWithIfds(listOf(ep)).first sample to traces } @@ -133,4 +141,17 @@ class TestAnalysisRunner( cfg = JIRMethodExitRuleProvider(cfg) return cfg } + + companion object { + /** + * Mirrors the Go sample harness ([GoSampleBasedTestBase]): unrolls the whole-object + * any-accessor taint of a starred source/sink down to concrete field and element reads. + * Opt in per-sample (e.g. the starred-SOURCE sample) so a source-star's any-field taint + * reaches a concrete field read; the default stays [AnyAccessorUnrollStrategy.AnyAccessorDisabled]. + */ + val AnyAccessorEnabled: AnyAccessorUnrollStrategy = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + } } 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..9ea82c6d5 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 @@ -66,6 +66,7 @@ import org.opentaint.dataflow.configuration.mkFalse import org.opentaint.dataflow.configuration.mkOr import org.opentaint.dataflow.configuration.mkTrue import org.opentaint.dataflow.configuration.simplify +import org.opentaint.dataflow.jvm.ap.ifds.taint.ContainsMarkOnAnyField import org.opentaint.ir.api.jvm.JIRAnnotated import org.opentaint.ir.api.jvm.JIRAnnotation import org.opentaint.ir.api.jvm.JIRClassType @@ -309,6 +310,7 @@ class MethodTaintConfigurationResolver( is SerializedCondition.ConstantLt -> pos.collectAnyArgumentClassifiers(classifiers) is SerializedCondition.ConstantMatches -> pos.collectAnyArgumentClassifiers(classifiers) is SerializedCondition.ContainsMark -> pos.collectAnyArgumentClassifiers(classifiers) + is SerializedCondition.ContainsMarkOnAnyField -> pos.collectAnyArgumentClassifiers(classifiers) is SerializedCondition.IsConstant -> isConstant.collectAnyArgumentClassifiers(classifiers) is SerializedCondition.IsNull -> isNull.collectAnyArgumentClassifiers(classifiers) is SerializedCondition.IsType -> pos.collectAnyArgumentClassifiers(classifiers) @@ -444,9 +446,22 @@ class MethodTaintConfigurationResolver( } is SerializedCondition.ContainsMark -> mkOr( + pos.resolvePosition(ctx).flatMap { p -> + val mark = taintMarkManager.taintMark(tainted) + buildList { + add(ContainsMark(p, mark).atom()) + // Star-model replacement for the array/vararg element-taint machinery: an + // array/vararg-typed position observes element (`arg[*]`) taint via the + // recursive any-field check, which the production unroll bridges to concrete + // element/field reads. Subsumes the old element-only `[*]` condition twin. + if (p.isArrayOrObjectTyped()) add(ContainsMarkOnAnyField(p, mark).atom()) + } + } + ) + + is SerializedCondition.ContainsMarkOnAnyField -> mkOr( pos.resolvePosition(ctx) - .flatMap { it.resolveArrayPosition() } - .map { ContainsMark(it, taintMarkManager.taintMark(tainted)).atom() } + .map { ContainsMarkOnAnyField(it, taintMarkManager.taintMark(tainted)).atom() } ) is SerializedCondition.IsType -> resolveIsType(ctx) @@ -559,9 +574,23 @@ class MethodTaintConfigurationResolver( return listOf(position) } + // Array/vararg-typed source assigns the whole object AND its element taint. Kept as a + // concrete element (`[*]`) rather than any-field so the assign produces concrete facts + // that need no any-accessor unroll (harness-safe under AnyAccessorDisabled); the sink-side + // any-field CONDITION (ContainsMarkOnAnyField) is what recovers element/vararg reads. return listOf(position, PositionWithAccess(position, PositionAccessor.ElementAccessor)) } + private fun TypeName?.isArrayOrObject(): Boolean = + this != null && (isArray || this == objectTypeName) + + private fun Position.isArrayOrObjectTyped(): Boolean = when (this) { + is ClassStatic, is This -> false + is PositionWithAccess -> false + is Argument -> method.parameters.getOrNull(index)?.type.isArrayOrObject() + is Result -> method.returnType.isArrayOrObject() + } + private fun SerializedTaintPassAction.resolve(ctx: AnyArgSpecializationCtx): List = from.resolvePosition(ctx).flatMap { fromPos -> to.resolvePosition(ctx).map { toPos -> @@ -581,7 +610,7 @@ class MethodTaintConfigurationResolver( if (taintKind == null) { RemoveAllMarks(pos) } else { - RemoveMark(taintMarkManager.taintMark(taintKind), pos) + RemoveMark(taintMarkManager.taintMark(taintKind), pos, reach) } } diff --git a/core/samples/src/main/java/test/samples/CleanerDslControlFlowSample.java b/core/samples/src/main/java/test/samples/CleanerDslControlFlowSample.java new file mode 100644 index 000000000..ace747389 --- /dev/null +++ b/core/samples/src/main/java/test/samples/CleanerDslControlFlowSample.java @@ -0,0 +1,280 @@ +package test.samples; + +public class CleanerDslControlFlowSample { + public static class Node { + public Node child; + public Node sibling; + public Level2 k; + } + + public static class Level2 { + public Level3 k; + } + + public static class Level3 { + public Node value; + } + + public Node sourceM1() { + return new Node(); + } + + public Node sourceM2() { + return new Node(); + } + + public Node sourceM3() { + return new Node(); + } + + public Node sourceM4() { + return new Node(); + } + + public Node sourceM5() { + return new Node(); + } + + public void cleanM1Plain(Node value) { } + + public void cleanM1Any(Node value) { } + + public void cleanM2Any(Node value) { } + + public void cleanM3Any(Node value) { } + + public void cleanM4Any(Node value) { } + + public void cleanM5Any(Node value) { } + + public void cleanM12Any(Node value) { } + + public void cleanM34Any(Node value) { } + + public void cleanAllAny(Node value) { } + + public void sequentialMarks() { + Node value = sourceM1(); + value.child = sourceM2(); + value.k.k.value = sourceM3(); + sequenceStartSink(value); + + cleanM1Plain(value); + sequenceAfterM1Sink(value); + + cleanM2Any(value); + sequenceAfterM2Sink(value); + + value.sibling = sourceM4(); + sequenceAfterM4SourceSink(value); + + cleanM3Any(value); + sequenceAfterM3Sink(value); + + cleanM4Any(value); + sequenceAllCleanSink(value); + + value.k.k.value = sourceM1(); + cleanM1Plain(value); + sequenceNestedAfterPlainSink(value); + + cleanM1Any(value); + sequenceNestedAfterAnySink(value); + } + + public void divergentBranches(Node value, boolean firstBranch, boolean secondBranch) { + if (firstBranch) { + cleanM12Any(value); + } else { + cleanM34Any(value); + } + divergentJoinSink(value); + + cleanM5Any(value); + divergentAfterM5Sink(value); + + if (secondBranch) { + cleanM12Any(value); + } else { + helperCleanM12(value); + } + convergentJoinSink(value); + } + + public void earlyReturnSummaries(Node maybeValue, Node alwaysValue, boolean clean) { + Node maybeCleaned = maybeCleanM12(maybeValue, clean); + maybeCleanReturnSink(maybeCleaned); + + Node alwaysCleaned = alwaysCleanM12(alwaysValue, clean); + alwaysCleanReturnSink(alwaysCleaned); + } + + private Node maybeCleanM12(Node value, boolean clean) { + if (clean) { + cleanM12Any(value); + } + return value; + } + + private Node alwaysCleanM12(Node value, boolean direct) { + if (direct) { + cleanM12Any(value); + } else { + helperCleanM12(value); + } + return value; + } + + private void helperCleanM12(Node value) { + cleanM12Any(value); + } + + public void aliasesAndReassignment(Node value) { + Node alias = value; + cleanM12Any(alias); + aliasOriginalSink(value); + + Node oldAlias = alias; + alias = sourceM1(); + reassignedOldSink(oldAlias); + reassignedNewSink(alias); + + cleanAllAny(oldAlias); + unsanitizedOriginalSink(value); + independentReassignmentSink(alias); + } + + public void deepCleanerPipeline(Node cleanedValue, Node controlValue) { + Node cleaned = pipeline1(cleanedValue); + deepPipelineCleanedSink(cleaned); + + Node unchanged = identity1(controlValue); + deepPipelineControlSink(unchanged); + } + + private Node pipeline1(Node value) { + Node result = pipeline2(value); + cleanM1Any(result); + return result; + } + + private Node pipeline2(Node value) { + Node result = pipeline3(value); + cleanM2Any(result); + return result; + } + + private Node pipeline3(Node value) { + Node result = pipeline4(value); + cleanM3Any(result); + return result; + } + + private Node pipeline4(Node value) { + Node result = pipeline5(value); + cleanM4Any(result); + return result; + } + + private Node pipeline5(Node value) { + cleanM5Any(value); + return value; + } + + private Node identity1(Node value) { + return identity2(value); + } + + private Node identity2(Node value) { + return identity3(value); + } + + private Node identity3(Node value) { + return identity4(value); + } + + private Node identity4(Node value) { + return identity5(value); + } + + private Node identity5(Node value) { + return value; + } + + public void doWhileCleaner(Node value, boolean repeat) { + do { + cleanM1Any(value); + if (repeat) { + cleanM2Any(value); + } + } while (repeat); + doWhileSink(value); + } + + public void zeroOrMoreCleaner(Node value, boolean repeat) { + while (repeat) { + cleanM34Any(value); + } + zeroOrMoreSink(value); + } + + public void independentBranchValues(Node left, Node right, boolean firstBranch) { + if (firstBranch) { + cleanM12Any(left); + cleanM34Any(right); + } else { + cleanM34Any(left); + cleanM12Any(right); + } + + independentLeftJoinSink(left); + independentRightJoinSink(right); + + cleanAllAny(left); + independentLeftCleanedSink(left); + independentRightUnchangedSink(right); + } + + public void cleanThenRetain() { + Node value = new Node(); + value.child = sourceM1(); + cleanAllAny(value); + cleanBeforeNewSourceSink(value); + + value.child = sourceM2(); + newSourceAfterCleanSink(value); + + cleanM2Any(value); + newSourceCleanedSink(value); + } + + public void sequenceStartSink(Node value) { } + public void sequenceAfterM1Sink(Node value) { } + public void sequenceAfterM2Sink(Node value) { } + public void sequenceAfterM4SourceSink(Node value) { } + public void sequenceAfterM3Sink(Node value) { } + public void sequenceAllCleanSink(Node value) { } + public void sequenceNestedAfterPlainSink(Node value) { } + public void sequenceNestedAfterAnySink(Node value) { } + public void divergentJoinSink(Node value) { } + public void divergentAfterM5Sink(Node value) { } + public void convergentJoinSink(Node value) { } + public void maybeCleanReturnSink(Node value) { } + public void alwaysCleanReturnSink(Node value) { } + public void aliasOriginalSink(Node value) { } + public void reassignedOldSink(Node value) { } + public void reassignedNewSink(Node value) { } + public void unsanitizedOriginalSink(Node value) { } + public void independentReassignmentSink(Node value) { } + public void deepPipelineCleanedSink(Node value) { } + public void deepPipelineControlSink(Node value) { } + public void doWhileSink(Node value) { } + public void zeroOrMoreSink(Node value) { } + public void independentLeftJoinSink(Node value) { } + public void independentRightJoinSink(Node value) { } + public void independentLeftCleanedSink(Node value) { } + public void independentRightUnchangedSink(Node value) { } + public void cleanBeforeNewSourceSink(Node value) { } + public void newSourceAfterCleanSink(Node value) { } + public void newSourceCleanedSink(Node value) { } +} diff --git a/core/samples/src/main/java/test/samples/CleanerDslSample.java b/core/samples/src/main/java/test/samples/CleanerDslSample.java new file mode 100644 index 000000000..5ce3e9d58 --- /dev/null +++ b/core/samples/src/main/java/test/samples/CleanerDslSample.java @@ -0,0 +1,531 @@ +package test.samples; + +public class CleanerDslSample { + public interface MatrixValue { } + + public static class Node implements MatrixValue { + public Level2 k; + public Node child; + } + + public static class Level2 implements MatrixValue { + public Level3 k; + public Node p; + } + + public static class Level3 implements MatrixValue { + public Level4 k; + } + + public static class Level4 implements MatrixValue { + public Level5 k; + } + + public static class Level5 implements MatrixValue { + public Level6 k; + } + + public static class Level6 implements MatrixValue { + } + + public Node sourcePlain() { + return new Node(); + } + + public Node sourceAny() { + return new Node(); + } + + public Node cleanPlain(Node value) { + return value; + } + + public Node cleanAny(Node value) { + return value; + } + + public void applyPlainClean(Node value) { } + + public void applyAnyClean(Node value) { } + + public void plainMarks1(Node plainCleaned, Node anyCleaned) { + plainPlain(plainCleaned); + plainAny(anyCleaned); + } + + public void plainMarks2(Node plainCleaned, Node anyCleaned) { + plainPlain(plainCleaned); + plainAny(anyCleaned); + } + + public void plainMarks3(Node plainCleaned, Node anyCleaned) { + plainPlain(plainCleaned); + plainAny(anyCleaned); + } + + public void plainMarks4(Node plainCleaned, Node anyCleaned) { + plainPlain(plainCleaned); + plainAny(anyCleaned); + } + + public void plainMarks5(Node plainCleaned, Node anyCleaned) { + plainPlain(plainCleaned); + plainAny(anyCleaned); + } + + public void anyMarks1(Node plainCleaned, Node anyCleaned) { + anyPlain(plainCleaned); + anyAny(anyCleaned); + } + + public void anyMarks2(Node plainCleaned, Node anyCleaned) { + anyPlain(plainCleaned); + anyAny(anyCleaned); + } + + public void anyMarks3(Node plainCleaned, Node anyCleaned) { + anyPlain(plainCleaned); + anyAny(anyCleaned); + } + + public void anyMarks4(Node plainCleaned, Node anyCleaned) { + anyPlain(plainCleaned); + anyAny(anyCleaned); + } + + public void anyMarks5(Node plainCleaned, Node anyCleaned) { + anyPlain(plainCleaned); + anyAny(anyCleaned); + } + + public void cleanMarks1(Node value) { + applyAnyClean(value); + markSelectiveSink(value); + } + + public void cleanMarks2(Node value) { + applyAnyClean(value); + markSelectiveSink(value); + } + + public void cleanMarks3(Node value) { + applyAnyClean(value); + markSelectiveSink(value); + } + + public void cleanMarks4(Node value) { + applyAnyClean(value); + markSelectiveSink(value); + } + + public void cleanMarks5(Node value) { + applyAnyClean(value); + markSelectiveSink(value); + } + + // Four source/cleaner pairs. Each pair checks both sink forms at field depth 0..5 and then + // sends the depth-1 field through helper stacks 1..5. + + private void plainPlain(Node value) { + applyPlainClean(value); + sinkPlainPlainPlainDepth0(value); + sinkPlainPlainAnyDepth0(value); + sinkPlainPlainPlainDepth1(value.k); + sinkPlainPlainAnyDepth1(value.k); + sinkPlainPlainPlainDepth2(value.k.k); + sinkPlainPlainAnyDepth2(value.k.k); + sinkPlainPlainPlainDepth3(value.k.k.k); + sinkPlainPlainAnyDepth3(value.k.k.k); + sinkPlainPlainPlainDepth4(value.k.k.k.k); + sinkPlainPlainAnyDepth4(value.k.k.k.k); + sinkPlainPlainPlainDepth5(value.k.k.k.k.k); + sinkPlainPlainAnyDepth5(value.k.k.k.k.k); + plainPlainStackDepth1(value); + } + + private void plainPlainStackDepth1(Node value) { + sinkPlainPlainPlainStackDepth1(value.k); + sinkPlainPlainAnyStackDepth1(value.k); + plainPlainStackDepth2(value); + } + + private void plainPlainStackDepth2(Node value) { + sinkPlainPlainPlainStackDepth2(value.k); + sinkPlainPlainAnyStackDepth2(value.k); + plainPlainStackDepth3(value); + } + + private void plainPlainStackDepth3(Node value) { + sinkPlainPlainPlainStackDepth3(value.k); + sinkPlainPlainAnyStackDepth3(value.k); + plainPlainStackDepth4(value); + } + + private void plainPlainStackDepth4(Node value) { + sinkPlainPlainPlainStackDepth4(value.k); + sinkPlainPlainAnyStackDepth4(value.k); + plainPlainStackDepth5(value); + } + + private void plainPlainStackDepth5(Node value) { + sinkPlainPlainPlainStackDepth5(value.k); + sinkPlainPlainAnyStackDepth5(value.k); + } + + private void plainAny(Node value) { + applyAnyClean(value); + sinkPlainAnyPlainDepth0(value); + sinkPlainAnyAnyDepth0(value); + sinkPlainAnyPlainDepth1(value.k); + sinkPlainAnyAnyDepth1(value.k); + sinkPlainAnyPlainDepth2(value.k.k); + sinkPlainAnyAnyDepth2(value.k.k); + sinkPlainAnyPlainDepth3(value.k.k.k); + sinkPlainAnyAnyDepth3(value.k.k.k); + sinkPlainAnyPlainDepth4(value.k.k.k.k); + sinkPlainAnyAnyDepth4(value.k.k.k.k); + sinkPlainAnyPlainDepth5(value.k.k.k.k.k); + sinkPlainAnyAnyDepth5(value.k.k.k.k.k); + plainAnyStackDepth1(value); + } + + private void plainAnyStackDepth1(Node value) { + sinkPlainAnyPlainStackDepth1(value.k); + sinkPlainAnyAnyStackDepth1(value.k); + plainAnyStackDepth2(value); + } + + private void plainAnyStackDepth2(Node value) { + sinkPlainAnyPlainStackDepth2(value.k); + sinkPlainAnyAnyStackDepth2(value.k); + plainAnyStackDepth3(value); + } + + private void plainAnyStackDepth3(Node value) { + sinkPlainAnyPlainStackDepth3(value.k); + sinkPlainAnyAnyStackDepth3(value.k); + plainAnyStackDepth4(value); + } + + private void plainAnyStackDepth4(Node value) { + sinkPlainAnyPlainStackDepth4(value.k); + sinkPlainAnyAnyStackDepth4(value.k); + plainAnyStackDepth5(value); + } + + private void plainAnyStackDepth5(Node value) { + sinkPlainAnyPlainStackDepth5(value.k); + sinkPlainAnyAnyStackDepth5(value.k); + } + + private void anyPlain(Node value) { + applyPlainClean(value); + sinkAnyPlainPlainDepth0(value); + sinkAnyPlainAnyDepth0(value); + sinkAnyPlainPlainDepth1(value.k); + sinkAnyPlainAnyDepth1(value.k); + sinkAnyPlainPlainDepth2(value.k.k); + sinkAnyPlainAnyDepth2(value.k.k); + sinkAnyPlainPlainDepth3(value.k.k.k); + sinkAnyPlainAnyDepth3(value.k.k.k); + sinkAnyPlainPlainDepth4(value.k.k.k.k); + sinkAnyPlainAnyDepth4(value.k.k.k.k); + sinkAnyPlainPlainDepth5(value.k.k.k.k.k); + sinkAnyPlainAnyDepth5(value.k.k.k.k.k); + anyPlainStackDepth1(value); + } + + private void anyPlainStackDepth1(Node value) { + sinkAnyPlainPlainStackDepth1(value.k); + sinkAnyPlainAnyStackDepth1(value.k); + anyPlainStackDepth2(value); + } + + private void anyPlainStackDepth2(Node value) { + sinkAnyPlainPlainStackDepth2(value.k); + sinkAnyPlainAnyStackDepth2(value.k); + anyPlainStackDepth3(value); + } + + private void anyPlainStackDepth3(Node value) { + sinkAnyPlainPlainStackDepth3(value.k); + sinkAnyPlainAnyStackDepth3(value.k); + anyPlainStackDepth4(value); + } + + private void anyPlainStackDepth4(Node value) { + sinkAnyPlainPlainStackDepth4(value.k); + sinkAnyPlainAnyStackDepth4(value.k); + anyPlainStackDepth5(value); + } + + private void anyPlainStackDepth5(Node value) { + sinkAnyPlainPlainStackDepth5(value.k); + sinkAnyPlainAnyStackDepth5(value.k); + } + + private void anyAny(Node value) { + applyAnyClean(value); + sinkAnyAnyPlainDepth0(value); + sinkAnyAnyAnyDepth0(value); + sinkAnyAnyPlainDepth1(value.k); + sinkAnyAnyAnyDepth1(value.k); + sinkAnyAnyPlainDepth2(value.k.k); + sinkAnyAnyAnyDepth2(value.k.k); + sinkAnyAnyPlainDepth3(value.k.k.k); + sinkAnyAnyAnyDepth3(value.k.k.k); + sinkAnyAnyPlainDepth4(value.k.k.k.k); + sinkAnyAnyAnyDepth4(value.k.k.k.k); + sinkAnyAnyPlainDepth5(value.k.k.k.k.k); + sinkAnyAnyAnyDepth5(value.k.k.k.k.k); + anyAnyStackDepth1(value); + } + + private void anyAnyStackDepth1(Node value) { + sinkAnyAnyPlainStackDepth1(value.k); + sinkAnyAnyAnyStackDepth1(value.k); + anyAnyStackDepth2(value); + } + + private void anyAnyStackDepth2(Node value) { + sinkAnyAnyPlainStackDepth2(value.k); + sinkAnyAnyAnyStackDepth2(value.k); + anyAnyStackDepth3(value); + } + + private void anyAnyStackDepth3(Node value) { + sinkAnyAnyPlainStackDepth3(value.k); + sinkAnyAnyAnyStackDepth3(value.k); + anyAnyStackDepth4(value); + } + + private void anyAnyStackDepth4(Node value) { + sinkAnyAnyPlainStackDepth4(value.k); + sinkAnyAnyAnyStackDepth4(value.k); + anyAnyStackDepth5(value); + } + + private void anyAnyStackDepth5(Node value) { + sinkAnyAnyPlainStackDepth5(value.k); + sinkAnyAnyAnyStackDepth5(value.k); + } + + // Direct translations of the field-store, nested-cleaner, helper-source, helper-sink, and + // branch-join examples. These use method sources rather than entry-point sources. + + public void fieldStoreExamples() { + Node plainRoot = new Node(); + plainRoot.child = sourcePlain(); + Node plainCleaned = cleanPlain(plainRoot); + fieldStorePlainSink(plainCleaned); + fieldStoreAnySink(plainCleaned); + + Node anyCleaned = cleanAny(plainRoot); + fieldStoreAfterAnyCleanSink(anyCleaned); + } + + public void nestedHelperCleanerExample() { + Node value = new Node(); + value.k.p = sourceAny(); + Node cleaned = helperAnyClean(value); + nestedHelperAnySink(cleaned); + } + + private Node helperAnyClean(Node value) { + Node cleaned = new Node(); + cleaned.k.p = cleanAny(value.k.p); + return cleaned; + } + + public void helperSourceAndCleanerExample() { + Node value = new Node(); + value.child = helperSource(); + Node cleaned = cleanAny(value); + helperSourceAnySink(cleaned); + } + + private Node helperSource() { + return sourcePlain(); + } + + public void helperSinkExample() { + Node value = new Node(); + value.child = helperSource(); + Node cleaned = cleanAny(value); + helperAnySink(cleaned); + } + + private void helperAnySink(Node value) { + helperSinkAnySink(value.child); + } + + public void conditionalExample(boolean flag) { + Node a = sourceA(); + Node x = sourceX(); + Node b; + Node y; + if (flag) { + b = cleanA(a); + y = x; + } else { + b = a; + y = cleanX(x); + } + sinkA(b); + sinkX(y); + } + + public void returningPlainCleaner(Node value) { + Node cleaned = cleanPlain(value); + returningPlainSink(cleaned); + } + + public void returningAnyCleaner(Node value) { + Node cleaned = cleanAny(value); + returningAnySink(cleaned); + } + + public void anyOnlySourceExample() { + Node value = sourceAnyOnly(); + anyOnlyRootSink(value); + anyOnlyChildSink(value.child); + } + + public void recursiveAnyOnlyStoreExample() { + Node root = new Node(); + root.child = sourceAnyOnly(); + recursiveAnyOnlyRootSink(root); + recursiveAnyOnlyChildSink(root.child); + recursiveAnyOnlyDepth2Sink(root.child.child); + } + + public Node sourceAnyOnly() { + return new Node(); + } + + public Node sourceA() { + return new Node(); + } + + public Node sourceX() { + return new Node(); + } + + public Node cleanA(Node value) { + return value; + } + + public Node cleanX(Node value) { + return value; + } + + public void returningPlainSink(Node value) { } + public void returningAnySink(Node value) { } + public void anyOnlyRootSink(Node value) { } + public void anyOnlyChildSink(Node value) { } + public void recursiveAnyOnlyRootSink(Node value) { } + public void recursiveAnyOnlyChildSink(Node value) { } + public void recursiveAnyOnlyDepth2Sink(Node value) { } + + // Every matrix endpoint has a distinct method so its rule id identifies one exact coordinate. + + public void sinkPlainPlainPlainDepth0(MatrixValue value) { } + public void sinkPlainPlainPlainDepth1(MatrixValue value) { } + public void sinkPlainPlainPlainDepth2(MatrixValue value) { } + public void sinkPlainPlainPlainDepth3(MatrixValue value) { } + public void sinkPlainPlainPlainDepth4(MatrixValue value) { } + public void sinkPlainPlainPlainDepth5(MatrixValue value) { } + public void sinkPlainPlainAnyDepth0(MatrixValue value) { } + public void sinkPlainPlainAnyDepth1(MatrixValue value) { } + public void sinkPlainPlainAnyDepth2(MatrixValue value) { } + public void sinkPlainPlainAnyDepth3(MatrixValue value) { } + public void sinkPlainPlainAnyDepth4(MatrixValue value) { } + public void sinkPlainPlainAnyDepth5(MatrixValue value) { } + public void sinkPlainAnyPlainDepth0(MatrixValue value) { } + public void sinkPlainAnyPlainDepth1(MatrixValue value) { } + public void sinkPlainAnyPlainDepth2(MatrixValue value) { } + public void sinkPlainAnyPlainDepth3(MatrixValue value) { } + public void sinkPlainAnyPlainDepth4(MatrixValue value) { } + public void sinkPlainAnyPlainDepth5(MatrixValue value) { } + public void sinkPlainAnyAnyDepth0(MatrixValue value) { } + public void sinkPlainAnyAnyDepth1(MatrixValue value) { } + public void sinkPlainAnyAnyDepth2(MatrixValue value) { } + public void sinkPlainAnyAnyDepth3(MatrixValue value) { } + public void sinkPlainAnyAnyDepth4(MatrixValue value) { } + public void sinkPlainAnyAnyDepth5(MatrixValue value) { } + public void sinkAnyPlainPlainDepth0(MatrixValue value) { } + public void sinkAnyPlainPlainDepth1(MatrixValue value) { } + public void sinkAnyPlainPlainDepth2(MatrixValue value) { } + public void sinkAnyPlainPlainDepth3(MatrixValue value) { } + public void sinkAnyPlainPlainDepth4(MatrixValue value) { } + public void sinkAnyPlainPlainDepth5(MatrixValue value) { } + public void sinkAnyPlainAnyDepth0(MatrixValue value) { } + public void sinkAnyPlainAnyDepth1(MatrixValue value) { } + public void sinkAnyPlainAnyDepth2(MatrixValue value) { } + public void sinkAnyPlainAnyDepth3(MatrixValue value) { } + public void sinkAnyPlainAnyDepth4(MatrixValue value) { } + public void sinkAnyPlainAnyDepth5(MatrixValue value) { } + public void sinkAnyAnyPlainDepth0(MatrixValue value) { } + public void sinkAnyAnyPlainDepth1(MatrixValue value) { } + public void sinkAnyAnyPlainDepth2(MatrixValue value) { } + public void sinkAnyAnyPlainDepth3(MatrixValue value) { } + public void sinkAnyAnyPlainDepth4(MatrixValue value) { } + public void sinkAnyAnyPlainDepth5(MatrixValue value) { } + public void sinkAnyAnyAnyDepth0(MatrixValue value) { } + public void sinkAnyAnyAnyDepth1(MatrixValue value) { } + public void sinkAnyAnyAnyDepth2(MatrixValue value) { } + public void sinkAnyAnyAnyDepth3(MatrixValue value) { } + public void sinkAnyAnyAnyDepth4(MatrixValue value) { } + public void sinkAnyAnyAnyDepth5(MatrixValue value) { } + + public void sinkPlainPlainPlainStackDepth1(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth2(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth3(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth4(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth5(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth1(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth2(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth3(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth4(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth5(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth1(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth2(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth3(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth4(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth5(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth1(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth2(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth3(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth4(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth5(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth1(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth2(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth3(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth4(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth5(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth1(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth2(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth3(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth4(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth5(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth1(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth2(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth3(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth4(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth5(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth1(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth2(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth3(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth4(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth5(MatrixValue value) { } + + public void fieldStorePlainSink(Node value) { } + public void fieldStoreAnySink(Node value) { } + public void fieldStoreAfterAnyCleanSink(Node value) { } + public void nestedHelperAnySink(Node value) { } + public void helperSourceAnySink(Node value) { } + public void helperSinkAnySink(Node value) { } + public void markSelectiveSink(Node value) { } + public void sinkA(Object value) { } + public void sinkX(Object value) { } +} diff --git a/core/samples/src/main/java/test/samples/DeepCleanSummarySample.java b/core/samples/src/main/java/test/samples/DeepCleanSummarySample.java new file mode 100644 index 000000000..ce9f3edaf --- /dev/null +++ b/core/samples/src/main/java/test/samples/DeepCleanSummarySample.java @@ -0,0 +1,146 @@ +package test.samples; + +public class DeepCleanSummarySample { + public static class Box { + public String f; + } + + public static class Pair { + public Box raw; + public Box val; + } + + public void clean(Box b) { } + public void sink(String data) { } + + Pair wrap(Box b) { + Pair p = new Pair(); + p.raw = b; + clean(b); + p.val = b; + return p; + } + + public void cleanedFlow(Box b) { + Pair p = wrap(b); + sink(p.val.f); + } + + public void uncleanedFlow(Box b) { + Pair p = wrap(b); + sink(p.raw.f); + } + + Pair wrapConditional(Box b, boolean flag) { + Pair p = new Pair(); + if (flag) { + clean(b); + } + p.val = b; + return p; + } + + public void conditionalCleanFlow(Box b, boolean flag) { + Pair p = wrapConditional(b, flag); + sink(p.val.f); + } + + Box wrapCleanOnly(Box b) { + clean(b); + return b; + } + + public void cleanOnlyFlow(Box b) { + Box r = wrapCleanOnly(b); + sink(r.f); + } + + // The whole cleaned flow inside ONE summarized helper: clean, then read, then return the + // read value. The claim never has to reach the entry point's own fact -- it must be + // effective inside the helper's summary computation. + String helperCleanThenRead(Box b) { + clean(b); + return b.f; + } + + public void helperCleanReadFlow(Box b) { + sink(helperCleanThenRead(b)); + } + + // Non-vacuity control for the same frame shape: no clean, the read must report. + String helperReadOnly(Box b) { + return b.f; + } + + public void helperReadFlow(Box b) { + sink(helperReadOnly(b)); + } + + // Same, with the clean one summary level deeper. + String helperNestedCleanThenRead(Box b) { + Box c = wrapCleanOnly(b); + return c.f; + } + + public void helperNestedCleanReadFlow(Box b) { + sink(helperNestedCleanThenRead(b)); + } + + public void sinkBox(Box b) { } + + public void boxCleanedFlow(Box b) { + Pair p = wrap(b); + sinkBox(p.val); + } + + public void boxUncleanedFlow(Box b) { + Pair p = wrap(b); + sinkBox(p.raw); + } + + public static class Leaf { + public String k; + } + + public static class Node { + public Leaf f; + } + + public static class NodePair { + public Node raw; + public Node val; + } + + public void cleanNode(Node b) { } + + NodePair wrapNode(Node b) { + NodePair p = new NodePair(); + p.raw = b; + cleanNode(b); + p.val = b; + return p; + } + + public void nodeCleanedFlow(Node b) { + NodePair p = wrapNode(b); + sink(p.val.f.k); + } + + public void nodeUncleanedFlow(Node b) { + NodePair p = wrapNode(b); + sink(p.raw.f.k); + } + + // Clean plus a depth-2 constant store inside one summarized helper; the object is + // returned and the caller reads the stored path. + Node cleanThenAssignLeaf(Node b) { + cleanNode(b); + b.f.k = "safe"; + return b; + } + + public void nodeCleanAssignFlow(Node b) { + Node r = cleanThenAssignLeaf(b); + sink(r.f.k); + } +} 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..dbdb7de20 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 @@ -107,6 +107,10 @@ abstract class AnalysisTest : BasicTestUtils() { open val useDefaultConfig = false + open val apMode: ApMode = ApMode.Tree + + open val analysisUnrollStrategy: AnyAccessorUnrollStrategy = AnyAccessorUnrollStrategy.AnyAccessorDisabled + private class SingleLocationUnit(val loc: RegisteredLocation) : JIRUnitResolver { override fun resolve(method: JIRMethod): UnitType { if (method.enclosingClass.declaration.location == loc || isApproximation(method)) { @@ -149,12 +153,12 @@ abstract class AnalysisTest : BasicTestUtils() { val options = TaintAnalyzerOptions( ifdsTimeout = 1.minutes, - ifdsApMode = ApMode.Tree + ifdsApMode = apMode ) val analyzer = object : TaintAnalyzer(options) { override val unrollStrategy: AnyAccessorUnrollStrategy - get() = AnyAccessorUnrollStrategy.AnyAccessorDisabled + get() = analysisUnrollStrategy override fun analysisGraph(): ApplicationGraph = ifdsGraph override fun analysisManager() = JIRAnalysisManager(cp, refManager, rulesProvider) diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerDslAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerDslAnalysisTest.kt new file mode 100644 index 000000000..9be37450e --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerDslAnalysisTest.kt @@ -0,0 +1,558 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +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.SerializedTaintCleanAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SinkMetaData + +/** + * Executable specification of the cleaner DSL. + * + * Each matrix run covers every plain/AnyField source, cleaner, and sink combination. It also + * reaches the value itself, field depths 1..5, and a depth-1 field hidden behind call stacks 1..5. + * The parameter controls how many independent marks coexist in the facts. + */ +class CleanerDslAnalysisTest : AnalysisTest() { + private companion object { + const val TEST_CLS = "test.samples.CleanerDslSample" + const val MATRIX_RULE_PREFIX = "cleaner-dsl-matrix" + } + + override val sourceFileExtension: String = "java" + + override val analysisUnrollStrategy: AnyAccessorUnrollStrategy = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + + private enum class Reach(val methodPart: String) { + Plain("Plain"), + AnyField("Any"), + } + + private data class MatrixCase( + val source: Reach, + val cleaner: Reach, + val sink: Reach, + ) { + val sinkMethodPrefix: String + get() = "sink${source.methodPart}${cleaner.methodPart}${sink.methodPart}" + + val id: String + get() = "${source.name}-${cleaner.name}-${sink.name}" + } + + private val matrixCases = Reach.entries.flatMap { source -> + Reach.entries.flatMap { cleaner -> + Reach.entries.map { sink -> MatrixCase(source, cleaner, sink) } + } + } + + private data class MatrixPoint( + val methodSuffix: String, + val id: String, + val fieldDepth: Int, + ) + + private val matrixPoints = + (0..5).map { depth -> + MatrixPoint(methodSuffix = "Depth$depth", id = "field-depth$depth", fieldDepth = depth) + } + + (1..5).map { depth -> + MatrixPoint(methodSuffix = "StackDepth$depth", id = "stack-depth$depth", fieldDepth = 1) + } + + private fun marks(count: Int): List = + (1..count).map { "mark$it" } + + private fun positions(base: PositionBase, reach: Reach): List = + buildList { + add(PositionBaseWithModifiers.BaseOnly(base)) + if (reach == Reach.AnyField) { + add( + PositionBaseWithModifiers.WithModifiers( + base, + listOf(PositionModifier.AnyField), + ) + ) + } + } + + private fun sourceRule(method: String, reach: Reach, marks: List) = + SerializedRule.Source( + function = functionMatcher(TEST_CLS, method), + taint = marks.flatMap { mark -> + positions(PositionBase.Result, reach).map { position -> + SerializedTaintAssignAction(kind = mark, pos = position) + } + } + ) + + private fun anyOnlySourceRule(method: String, mark: String) = + SerializedRule.Source( + function = functionMatcher(TEST_CLS, method), + taint = listOf( + SerializedTaintAssignAction( + kind = mark, + pos = PositionBaseWithModifiers.WithModifiers( + PositionBase.Result, + listOf(PositionModifier.AnyField), + ), + ) + ), + ) + + private fun cleanerRule( + method: String, + reach: Reach, + marks: List, + cleansResult: Boolean = false, + ) = + SerializedRule.Cleaner( + function = functionMatcher(TEST_CLS, method), + cleans = marks.flatMap { mark -> + buildList { + addAll(positions(Argument(0), reach)) + if (cleansResult) addAll(positions(PositionBase.Result, reach)) + }.map { position -> + SerializedTaintCleanAction( + taintKind = mark, + pos = position, + ) + } + } + ) + + private fun sinkRule( + method: String, + reach: Reach, + mark: String, + ruleId: String, + ) = SerializedRule.Sink( + condition = SerializedCondition.or( + positions(Argument(0), reach).map { position -> + SerializedCondition.ContainsMark(mark, position) + } + ), + function = functionMatcher(TEST_CLS, method), + id = ruleId, + meta = SinkMetaData(note = ruleId), + ) + + private fun matrixRuleId(case: MatrixCase, point: MatrixPoint, mark: String): String = + "$MATRIX_RULE_PREFIX-${case.id}-${point.id}-$mark" + + private fun matrixEntryPoint( + markCount: Int, + sourceReach: Reach, + ) = SerializedRule.EntryPoint( + function = functionMatcher( + TEST_CLS, + "${sourceReach.methodPart.lowercase()}Marks$markCount", + ), + taint = listOf(Argument(0), Argument(1)).flatMap { argument -> + marks(markCount).flatMap { mark -> + positions(argument, sourceReach).map { position -> + SerializedTaintAssignAction(kind = mark, pos = position) + } + } + }, + ) + + private fun matrixConfig( + markCount: Int, + sourceReach: Reach, + cleanersEnabled: Boolean = true, + ): SerializedTaintConfig { + val marks = marks(markCount) + return SerializedTaintConfig( + entryPoint = listOf(matrixEntryPoint(markCount, sourceReach)), + cleaner = if (cleanersEnabled) { + listOf( + cleanerRule("applyPlainClean", Reach.Plain, marks), + cleanerRule("applyAnyClean", Reach.AnyField, marks), + ) + } else { + emptyList() + }, + sink = matrixCases.filter { it.source == sourceReach }.flatMap { case -> + matrixPoints.flatMap { point -> + marks.map { mark -> + sinkRule( + method = case.sinkMethodPrefix + point.methodSuffix, + reach = case.sink, + mark = mark, + ruleId = matrixRuleId(case, point, mark), + ) + } + } + }, + ) + } + + private fun matrixFindings( + markCount: Int, + sourceReach: Reach, + cleanersEnabled: Boolean = true, + ): Set = + runAnalysis( + config = matrixConfig(markCount, sourceReach, cleanersEnabled), + entryPointClass = TEST_CLS, + entryPointMethod = "${sourceReach.methodPart.lowercase()}Marks$markCount", + ).mapTo(hashSetOf()) { it.vulnerability.rule.id } + + @ParameterizedTest(name = "{0} simultaneous mark(s)") + @ValueSource(ints = [1, 2, 3, 4, 5]) + fun `plain and AnyField matrix is exact through field and call depths one to five`(markCount: Int) { + for (sourceReach in Reach.entries) { + val expected = buildSet { + for (case in matrixCases.filter { it.source == sourceReach }) { + for (point in matrixPoints) { + val survives = + case.source == Reach.AnyField && + case.cleaner == Reach.Plain && + (case.sink == Reach.AnyField || point.fieldDepth > 0) + + if (survives) { + marks(markCount).mapTo(this) { mark -> + matrixRuleId(case, point, mark) + } + } + } + } + } + + assertEquals(expected, matrixFindings(markCount, sourceReach)) + } + } + + @Test + fun `without cleaners AnyField sources reach both sink forms at every depth`() { + val findings = matrixFindings( + markCount = 5, + sourceReach = Reach.AnyField, + cleanersEnabled = false, + ) + val expectedControls = buildSet { + for (case in matrixCases.filter { it.source == Reach.AnyField }) { + for (point in matrixPoints) { + marks(5).mapTo(this) { mark -> + matrixRuleId(case, point, mark) + } + } + } + } + + assertEquals(expectedControls, findings) + } + + @ParameterizedTest(name = "clean {0} of 5 marks") + @ValueSource(ints = [1, 2, 3, 4, 5]) + fun `AnyField cleaner removes only the selected marks`(cleanCount: Int) { + val allMarks = marks(5) + val config = SerializedTaintConfig( + entryPoint = listOf( + SerializedRule.EntryPoint( + function = functionMatcher(TEST_CLS, "cleanMarks$cleanCount"), + taint = allMarks.flatMap { mark -> + positions(Argument(0), Reach.AnyField).map { position -> + SerializedTaintAssignAction(kind = mark, pos = position) + } + }, + ) + ), + cleaner = listOf( + cleanerRule("applyAnyClean", Reach.AnyField, allMarks.take(cleanCount)) + ), + sink = allMarks.map { mark -> + sinkRule( + "markSelectiveSink", + Reach.AnyField, + mark, + "mark-selective-$mark", + ) + }, + ) + + val findings = findingIds(config, "cleanMarks$cleanCount") + val expected = allMarks.drop(cleanCount).mapTo(hashSetOf()) { "mark-selective-$it" } + + assertEquals(expected, findings) + } + + @Test + fun `field stores distinguish a plain cleaner from an AnyField cleaner`() { + val mark = "field-store" + val config = SerializedTaintConfig( + source = listOf(sourceRule("sourcePlain", Reach.Plain, listOf(mark))), + cleaner = listOf( + cleanerRule("cleanPlain", Reach.Plain, listOf(mark), cleansResult = true), + cleanerRule("cleanAny", Reach.AnyField, listOf(mark), cleansResult = true), + ), + sink = listOf( + sinkRule("fieldStorePlainSink", Reach.Plain, mark, "field-store-plain"), + sinkRule("fieldStoreAnySink", Reach.AnyField, mark, "field-store-any"), + sinkRule("fieldStoreAfterAnyCleanSink", Reach.AnyField, mark, "field-store-cleaned"), + ), + ) + + val findings = runAnalysis(config, TEST_CLS, "fieldStoreExamples") + .mapTo(hashSetOf()) { it.vulnerability.rule.id } + + assertEquals(setOf("field-store-any"), findings) + } + + @Test + fun `nested helper AnyField cleaner removes a nested AnyField source`() { + assertHelperCleaned( + entryPoint = "nestedHelperCleanerExample", + sink = "nestedHelperAnySink", + mark = "nested-helper", + sourceReach = Reach.AnyField, + ) + } + + @Test + fun `helper source is cleaned before an AnyField sink`() { + assertHelperCleaned( + entryPoint = "helperSourceAndCleanerExample", + sink = "helperSourceAnySink", + mark = "helper-source", + sourceReach = Reach.Plain, + ) + } + + @Test + fun `AnyField sink hidden in a helper stays silent after cleaning`() { + assertHelperCleaned( + entryPoint = "helperSinkExample", + sink = "helperSinkAnySink", + mark = "helper-sink", + sourceReach = Reach.Plain, + ) + } + + private fun helperConfig( + entryPointSink: String, + mark: String, + sourceReach: Reach = Reach.AnyField, + cleanersEnabled: Boolean = true, + ) = SerializedTaintConfig( + source = listOf(sourceRule("source${sourceReach.methodPart}", sourceReach, listOf(mark))), + cleaner = if (cleanersEnabled) { + listOf( + cleanerRule( + "cleanAny", + Reach.AnyField, + listOf(mark), + cleansResult = true, + ) + ) + } else { + emptyList() + }, + sink = listOf(sinkRule(entryPointSink, Reach.AnyField, mark, "$mark-sink")), + ) + + private fun assertHelperCleaned( + entryPoint: String, + sink: String, + mark: String, + sourceReach: Reach, + ) { + assertEquals( + emptySet(), + findingIds(helperConfig(sink, mark, sourceReach), entryPoint), + ) + assertEquals( + setOf("$mark-sink"), + findingIds( + helperConfig(sink, mark, sourceReach, cleanersEnabled = false), + entryPoint, + ), + "The no-cleaner control must reach the same helper sink", + ) + } + + private fun findingIds( + config: SerializedTaintConfig, + entryPoint: String, + ): Set = + runAnalysis(config, TEST_CLS, entryPoint) + .mapTo(hashSetOf()) { it.vulnerability.rule.id } + + @Test + fun `branch-specific cleaners do not clean the opposite alternative at a join`() { + val aMark = "a-mark" + val xMark = "x-mark" + val config = SerializedTaintConfig( + source = listOf( + sourceRule("sourceA", Reach.AnyField, listOf(aMark)), + sourceRule("sourceX", Reach.AnyField, listOf(xMark)), + ), + cleaner = listOf( + cleanerRule("cleanA", Reach.AnyField, listOf(aMark), cleansResult = true), + cleanerRule("cleanX", Reach.AnyField, listOf(xMark), cleansResult = true), + ), + sink = listOf( + sinkRule("sinkA", Reach.AnyField, aMark, "conditional-a"), + sinkRule("sinkX", Reach.AnyField, xMark, "conditional-x"), + ), + ) + + val findings = runAnalysis(config, TEST_CLS, "conditionalExample") + .mapTo(hashSetOf()) { it.vulnerability.rule.id } + + assertEquals(setOf("conditional-a", "conditional-x"), findings) + } + + @Test + fun `returning exact cleaner preserves AnyField taint and unrelated marks`() { + val cleanedMark = "cleaned" + val unrelatedMark = "unrelated" + val config = SerializedTaintConfig( + entryPoint = listOf( + SerializedRule.EntryPoint( + function = functionMatcher(TEST_CLS, "returningPlainCleaner"), + taint = listOf(cleanedMark, unrelatedMark).flatMap { mark -> + positions(Argument(0), Reach.AnyField).map { position -> + SerializedTaintAssignAction(kind = mark, pos = position) + } + }, + ) + ), + cleaner = listOf( + cleanerRule( + "cleanPlain", + Reach.Plain, + listOf(cleanedMark), + cleansResult = true, + ) + ), + sink = listOf( + sinkRule( + "returningPlainSink", + Reach.AnyField, + cleanedMark, + "returning-cleaned-any", + ), + sinkRule( + "returningPlainSink", + Reach.Plain, + unrelatedMark, + "returning-unrelated-exact", + ), + ), + ) + + assertEquals( + setOf("returning-cleaned-any", "returning-unrelated-exact"), + findingIds(config, "returningPlainCleaner"), + ) + } + + @Test + fun `result AnyField cleaner does not abort an unrelated fact`() { + val cleanedMark = "absent" + val unrelatedMark = "unrelated" + val config = SerializedTaintConfig( + entryPoint = listOf( + SerializedRule.EntryPoint( + function = functionMatcher(TEST_CLS, "returningAnyCleaner"), + taint = positions(Argument(0), Reach.AnyField).map { position -> + SerializedTaintAssignAction(kind = unrelatedMark, pos = position) + }, + ) + ), + cleaner = listOf( + cleanerRule( + "cleanAny", + Reach.AnyField, + listOf(cleanedMark), + cleansResult = true, + ) + ), + sink = listOf( + sinkRule( + "returningAnySink", + Reach.Plain, + unrelatedMark, + "returning-any-unrelated", + ) + ), + ) + + assertEquals( + setOf("returning-any-unrelated"), + findingIds(config, "returningAnyCleaner"), + ) + } + + @Test + fun `AnyField-only source matches the root and materialized child`() { + val mark = "any-only" + val config = SerializedTaintConfig( + source = listOf(anyOnlySourceRule("sourceAnyOnly", mark)), + sink = listOf( + sinkRule("anyOnlyRootSink", Reach.AnyField, mark, "any-only-root"), + sinkRule("anyOnlyChildSink", Reach.Plain, mark, "any-only-child"), + ), + ) + + assertEquals( + setOf("any-only-root", "any-only-child"), + findingIds(config, "anyOnlySourceExample"), + ) + } + + @Test + fun `AnyField-only source survives a recursive field store`() { + val mark = "recursive-any-only" + val config = SerializedTaintConfig( + source = listOf(anyOnlySourceRule("sourceAnyOnly", mark)), + sink = listOf( + sinkRule( + "recursiveAnyOnlyRootSink", + Reach.AnyField, + mark, + "recursive-any-only-root", + ), + sinkRule( + "recursiveAnyOnlyChildSink", + Reach.AnyField, + mark, + "recursive-any-only-child", + ), + sinkRule( + "recursiveAnyOnlyDepth2Sink", + Reach.Plain, + mark, + "recursive-any-only-depth2", + ), + ), + ) + + assertEquals( + setOf( + "recursive-any-only-root", + "recursive-any-only-child", + "recursive-any-only-depth2", + ), + findingIds(config, "recursiveAnyOnlyStoreExample"), + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerDslControlFlowAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerDslControlFlowAnalysisTest.kt new file mode 100644 index 000000000..3731633a6 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerDslControlFlowAnalysisTest.kt @@ -0,0 +1,267 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +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.SerializedTaintCleanAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SinkMetaData + +/** + * Control-flow examples complementing [CleanerDslAnalysisTest]'s structural matrix. + * + * A checkpoint declares the complete set of marks that may survive there. The test DSL emits a + * separate sink for every mark, so an assertion detects both missing and unexpectedly retained + * facts. + */ +class CleanerDslControlFlowAnalysisTest : AnalysisTest() { + private companion object { + const val TEST_CLS = "test.samples.CleanerDslControlFlowSample" + val MARKS = (1..5).map { "m$it" } + } + + override val sourceFileExtension: String = "java" + + override val analysisUnrollStrategy: AnyAccessorUnrollStrategy = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + + private enum class Reach { + Plain, + AnyField, + } + + private data class Checkpoint( + val method: String, + val survivingMarks: Set, + ) { + val expectedRuleIds: Set + get() = survivingMarks.mapTo(hashSetOf()) { ruleId(it) } + + fun ruleId(mark: String): String = "$method-$mark" + } + + private fun checkpoint( + method: String, + vararg survivingMarks: String, + ) = Checkpoint(method, survivingMarks.toSet()) + + private fun positions( + base: PositionBase, + reach: Reach, + ): List = + buildList { + add(PositionBaseWithModifiers.BaseOnly(base)) + if (reach == Reach.AnyField) { + add( + PositionBaseWithModifiers.WithModifiers( + base, + listOf(PositionModifier.AnyField), + ) + ) + } + } + + private fun source( + method: String, + reach: Reach, + marks: List, + ) = SerializedRule.Source( + function = functionMatcher(TEST_CLS, method), + taint = marks.flatMap { mark -> + positions(PositionBase.Result, reach).map { position -> + SerializedTaintAssignAction(kind = mark, pos = position) + } + }, + ) + + private fun cleaner( + method: String, + reach: Reach, + marks: List, + ) = SerializedRule.Cleaner( + function = functionMatcher(TEST_CLS, method), + cleans = marks.flatMap { mark -> + positions(Argument(0), reach).map { position -> + SerializedTaintCleanAction(taintKind = mark, pos = position) + } + }, + ) + + private fun sink( + checkpoint: Checkpoint, + mark: String, + ) = SerializedRule.Sink( + condition = SerializedCondition.or( + positions(Argument(0), Reach.AnyField).map { position -> + SerializedCondition.ContainsMark(mark, position) + } + ), + function = functionMatcher(TEST_CLS, checkpoint.method), + id = checkpoint.ruleId(mark), + meta = SinkMetaData(note = checkpoint.ruleId(mark)), + ) + + private val sources = MARKS.mapIndexed { index, mark -> + source("sourceM${index + 1}", Reach.Plain, listOf(mark)) + } + + private val cleaners = listOf( + cleaner("cleanM1Plain", Reach.Plain, listOf("m1")), + cleaner("cleanM1Any", Reach.AnyField, listOf("m1")), + cleaner("cleanM2Any", Reach.AnyField, listOf("m2")), + cleaner("cleanM3Any", Reach.AnyField, listOf("m3")), + cleaner("cleanM4Any", Reach.AnyField, listOf("m4")), + cleaner("cleanM5Any", Reach.AnyField, listOf("m5")), + cleaner("cleanM12Any", Reach.AnyField, listOf("m1", "m2")), + cleaner("cleanM34Any", Reach.AnyField, listOf("m3", "m4")), + cleaner("cleanAllAny", Reach.AnyField, MARKS), + ) + + private fun anyFieldEntryPoint( + entryPoint: String, + arguments: List, + ) = SerializedRule.EntryPoint( + function = functionMatcher(TEST_CLS, entryPoint), + taint = arguments.flatMap { argument -> + MARKS.flatMap { mark -> + positions(Argument(argument), Reach.AnyField).map { position -> + SerializedTaintAssignAction(kind = mark, pos = position) + } + } + }, + ) + + private fun assertScenario( + entryPoint: String, + taintedArguments: List, + vararg checkpoints: Checkpoint, + ) { + val config = SerializedTaintConfig( + entryPoint = taintedArguments + .takeIf { it.isNotEmpty() } + ?.let { listOf(anyFieldEntryPoint(entryPoint, it)) }, + source = sources, + cleaner = cleaners, + sink = checkpoints.flatMap { checkpoint -> + MARKS.map { mark -> sink(checkpoint, mark) } + }, + ) + val actual = runAnalysis(config, TEST_CLS, entryPoint) + .mapTo(hashSetOf()) { it.vulnerability.rule.id } + val expected = checkpoints.flatMapTo(hashSetOf()) { it.expectedRuleIds } + + assertEquals(expected, actual) + } + + private fun assertSourceScenario( + entryPoint: String, + vararg checkpoints: Checkpoint, + ) = assertScenario(entryPoint, emptyList(), *checkpoints) + + @Test + fun `marks are accumulated and removed independently in a long sequence`() { + assertSourceScenario( + "sequentialMarks", + checkpoint("sequenceStartSink", "m1", "m2", "m3"), + checkpoint("sequenceAfterM1Sink", "m2", "m3"), + checkpoint("sequenceAfterM2Sink", "m3"), + checkpoint("sequenceAfterM4SourceSink", "m3", "m4"), + checkpoint("sequenceAfterM3Sink", "m4"), + checkpoint("sequenceAllCleanSink"), + checkpoint("sequenceNestedAfterPlainSink", "m1"), + checkpoint("sequenceNestedAfterAnySink"), + ) + } + + @Test + fun `different branch cleaners retain every mark surviving at least one path`() { + assertScenario( + "divergentBranches", + taintedArguments = listOf(0), + checkpoint("divergentJoinSink", *MARKS.toTypedArray()), + checkpoint("divergentAfterM5Sink", "m1", "m2", "m3", "m4"), + checkpoint("convergentJoinSink", "m3", "m4"), + ) + } + + @Test + fun `early-return summaries distinguish maybe-cleaned from always-cleaned values`() { + assertScenario( + "earlyReturnSummaries", + taintedArguments = listOf(0, 1), + checkpoint("maybeCleanReturnSink", *MARKS.toTypedArray()), + checkpoint("alwaysCleanReturnSink", "m3", "m4", "m5"), + ) + } + + @Test + fun `AnyField mark exclusions follow one alias without leaking to sibling facts`() { + assertScenario( + "aliasesAndReassignment", + taintedArguments = listOf(0), + checkpoint("aliasOriginalSink", *MARKS.toTypedArray()), + checkpoint("reassignedOldSink", "m3", "m4", "m5"), + checkpoint("reassignedNewSink", "m1"), + checkpoint("unsanitizedOriginalSink", *MARKS.toTypedArray()), + checkpoint("independentReassignmentSink", "m1"), + ) + } + + @Test + fun `five cleaners compose through five helper summaries`() { + assertScenario( + "deepCleanerPipeline", + taintedArguments = listOf(0, 1), + checkpoint("deepPipelineCleanedSink"), + checkpoint("deepPipelineControlSink", *MARKS.toTypedArray()), + ) + } + + @Test + fun `do-while cleaner applies once while a while cleaner may not apply`() { + assertScenario( + "doWhileCleaner", + taintedArguments = listOf(0), + checkpoint("doWhileSink", "m2", "m3", "m4", "m5"), + ) + assertScenario( + "zeroOrMoreCleaner", + taintedArguments = listOf(0), + checkpoint("zeroOrMoreSink", *MARKS.toTypedArray()), + ) + } + + @Test + fun `cleaners on independent branch values do not cross-contaminate`() { + assertScenario( + "independentBranchValues", + taintedArguments = listOf(0, 1), + checkpoint("independentLeftJoinSink", *MARKS.toTypedArray()), + checkpoint("independentRightJoinSink", *MARKS.toTypedArray()), + checkpoint("independentLeftCleanedSink"), + checkpoint("independentRightUnchangedSink", *MARKS.toTypedArray()), + ) + } + + @Test + fun `a clean does not suppress sources introduced later`() { + assertSourceScenario( + "cleanThenRetain", + checkpoint("cleanBeforeNewSourceSink"), + checkpoint("newSourceAfterCleanSink", "m2"), + checkpoint("newSourceCleanedSink"), + ) + } +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerFieldSensitivityAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerFieldSensitivityAnalysisTest.kt new file mode 100644 index 000000000..a47bfe565 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/CleanerFieldSensitivityAnalysisTest.kt @@ -0,0 +1,327 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.ApMode +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.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintCleanAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +/** + * How field-sensitive a cleaner is across a summarized wrapper, as a function of how its position + * is written. + * + * Every case runs the same program shape. `wrap`/`wrapNode` stores its whole argument twice -- + * once before the clean (`p.raw`) and once after it (`p.val`) -- so both stores alias one object + * and the two summary edges leave from the same initial fact. One sink reads below `p.raw` and + * must report; the sibling reads below `p.val` and must not. + * + * Held constant across a pair: the program, the source, the sink, the read depth. The only + * variable is the cleaner position, and it decides everything: + * + * - a CONCRETE position (`arg0`, `arg0.f`, `arg0.f.k`) names a path that exists in the access + * tree, so the clean is a node deletion. `.raw` and `.val` are different branches of that tree + * and stay apart through the summary merge, which is a tree merge. Correct at every depth, and + * correct even when the SOURCE is abstract: the demand-driven refinement splits an any-field + * fact into concrete facts until the cleaner's path is one of them, and each carries its own + * access path; + * - a STARRED position (`arg0.*`) names unboundedly many paths, so there is no single node to + * delete. In tree mode the clean is still structural ([FinalFactAp.deepClean]): concrete + * `![m]` nodes below the base are deleted outright, and each abstract node is annotated with + * the residual claim (AnyFieldMarkExclusions) that the mark stays excluded from whatever + * materializes below it. The claim is part of the node, so it travels with `.val` and never + * meets `.raw` -- the same branch discrimination the concrete clean gets from the tree. + * + * The starred cases pin that structural clean across a summary; a flat edge-level flag has no + * position in the tree and made exactly the deeper starred reads (depths 2 and 3) report false + * positives. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class CleanerFieldSensitivityAnalysisTest : AnalysisTest() { + + companion object { + private const val TEST_CLS = "test.samples.DeepCleanSummarySample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "cleaner-field-sensitivity-rule" + + private const val BOX = "test.samples.DeepCleanSummarySample\$Box" + private const val NODE = "test.samples.DeepCleanSummarySample\$Node" + private const val LEAF = "test.samples.DeepCleanSummarySample\$Leaf" + } + + override val sourceFileExtension: String = "java" + + override val analysisUnrollStrategy: AnyAccessorUnrollStrategy = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + + private val boxF = PositionModifier.Field(BOX, "f", "java.lang.String") + private val nodeF = PositionModifier.Field(NODE, "f", LEAF) + private val leafK = PositionModifier.Field(LEAF, "k", "java.lang.String") + + private fun source(entryPointMethod: String, vararg positions: PositionBaseWithModifiers) = + SerializedRule.EntryPoint( + function = functionMatcher(TEST_CLS, entryPointMethod), + taint = positions.map { SerializedTaintAssignAction(kind = TAINT_MARK, pos = it) } + ) + + private fun cleaner(cleanMethod: String, vararg positions: PositionBaseWithModifiers) = + SerializedRule.Cleaner( + function = functionMatcher(TEST_CLS, cleanMethod), + cleans = positions.map { SerializedTaintCleanAction(taintKind = TAINT_MARK, pos = it) } + ) + + private fun baseOnly() = PositionBaseWithModifiers.BaseOnly(Argument(0)) + + private fun withModifiers(vararg modifiers: PositionModifier) = + PositionBaseWithModifiers.WithModifiers(Argument(0), modifiers.toList()) + + private fun starred(cleanMethod: String) = + cleaner(cleanMethod, baseOnly(), withModifiers(PositionModifier.AnyField)) + + private fun config( + entryPointMethod: String, + sinkMethod: String, + source: SerializedRule.EntryPoint, + cleaner: SerializedRule.Cleaner, + ) = SerializedTaintConfig( + entryPoint = listOf(source), + cleaner = listOf(cleaner), + sink = listOf(sinkRule(TEST_CLS, sinkMethod, RULE_ID, listOf(Argument(0) to TAINT_MARK))) + ) + + /* ---------- depth 1: the sink reads the stored object itself ---------- */ + + private fun baseConfig(entryPointMethod: String) = config( + entryPointMethod, + sinkMethod = "sinkBox", + source = source(entryPointMethod, baseOnly()), + cleaner = cleaner("clean", baseOnly()) + ) + + private fun starredDepth1Config(entryPointMethod: String) = config( + entryPointMethod, + sinkMethod = "sinkBox", + source = source(entryPointMethod, baseOnly(), withModifiers(PositionModifier.AnyField)), + cleaner = starred("clean") + ) + + @Test + fun `concrete base clean - the unsanitized field reports`() = assertReachable( + config = baseConfig("boxUncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "boxUncleanedFlow", + ruleId = RULE_ID, + testName = "concrete base clean, unsanitized field" + ) + + @Test + open fun `concrete base clean - the sanitized field is silent`() = assertNotReachable( + config = baseConfig("boxCleanedFlow"), + testCls = TEST_CLS, + entryPointName = "boxCleanedFlow", + testName = "concrete base clean, sanitized field" + ) + + @Test + fun `starred clean at depth 1 - the unsanitized field reports`() = assertReachable( + config = starredDepth1Config("boxUncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "boxUncleanedFlow", + ruleId = RULE_ID, + testName = "starred clean depth 1, unsanitized field" + ) + + @Test + open fun `starred clean at depth 1 - the sanitized field is silent`() = assertNotReachable( + // Green, and it does not exercise the star: the read is `p.val` itself, which the starred + // cleaner's BASE component removes as a concrete node. The deep exclusion is not consulted. + config = starredDepth1Config("boxCleanedFlow"), + testCls = TEST_CLS, + entryPointName = "boxCleanedFlow", + testName = "starred clean depth 1, sanitized field" + ) + + /* ---------- depth 2: concrete source, concrete one-level clean ---------- */ + + private fun fieldConfig(entryPointMethod: String) = config( + entryPointMethod, + sinkMethod = "sink", + source = source(entryPointMethod, withModifiers(boxF)), + cleaner = cleaner("clean", withModifiers(boxF)) + ) + + private fun starredDepth2Config(entryPointMethod: String) = config( + entryPointMethod, + sinkMethod = "sink", + source = source(entryPointMethod, withModifiers(boxF)), + cleaner = starred("clean") + ) + + @Test + fun `concrete field clean - the unsanitized field reports`() = assertReachable( + config = fieldConfig("uncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "uncleanedFlow", + ruleId = RULE_ID, + testName = "concrete field clean, unsanitized field" + ) + + @Test + open fun `concrete field clean - the sanitized field is silent`() = assertNotReachable( + config = fieldConfig("cleanedFlow"), + testCls = TEST_CLS, + entryPointName = "cleanedFlow", + testName = "concrete field clean, sanitized field" + ) + + @Test + fun `starred clean at depth 2 - the unsanitized field reports`() = assertReachable( + config = starredDepth2Config("uncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "uncleanedFlow", + ruleId = RULE_ID, + testName = "starred clean depth 2, unsanitized field" + ) + + @Test + open fun `starred clean at depth 2 - the sanitized field is silent`() = assertNotReachable( + config = starredDepth2Config("cleanedFlow"), + testCls = TEST_CLS, + entryPointName = "cleanedFlow", + testName = "starred clean depth 2, sanitized field" + ) + + /* ---------- depth 3: ABSTRACT source, concrete two-level clean ---------- */ + + private fun deepFieldConfig(entryPointMethod: String) = config( + entryPointMethod, + sinkMethod = "sink", + source = source(entryPointMethod, withModifiers(PositionModifier.AnyField)), + cleaner = cleaner("cleanNode", withModifiers(nodeF, leafK)) + ) + + private fun starredDepth3Config(entryPointMethod: String) = config( + entryPointMethod, + sinkMethod = "sink", + source = source(entryPointMethod, withModifiers(PositionModifier.AnyField)), + cleaner = starred("cleanNode") + ) + + @Test + fun `concrete two-level clean over an abstract source - the unsanitized field reports`() = assertReachable( + config = deepFieldConfig("nodeUncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "nodeUncleanedFlow", + ruleId = RULE_ID, + testName = "concrete two-level clean, unsanitized field" + ) + + @Test + open fun `concrete two-level clean over an abstract source - the sanitized field is silent`() = assertNotReachable( + // The source is any-field, so the cleaner's path does not exist as a fact until the + // demand-driven refinement produces it. Once it does, the clean is a node deletion again + // and field sensitivity survives the summary -- an abstract source is not the problem. + config = deepFieldConfig("nodeCleanedFlow"), + testCls = TEST_CLS, + entryPointName = "nodeCleanedFlow", + testName = "concrete two-level clean, sanitized field" + ) + + @Test + fun `starred clean at depth 3 - the unsanitized field reports`() = assertReachable( + config = starredDepth3Config("nodeUncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "nodeUncleanedFlow", + ruleId = RULE_ID, + testName = "starred clean depth 3, unsanitized field" + ) + + @Test + open fun `starred clean at depth 3 - the sanitized field is silent`() = assertNotReachable( + config = starredDepth3Config("nodeCleanedFlow"), + testCls = TEST_CLS, + entryPointName = "nodeCleanedFlow", + testName = "starred clean depth 3, sanitized field" + ) + + /* ---------- non-vacuity controls ---------- */ + + /** + * Every `is silent` case above asserts an ABSENT finding, which an engine that simply loses the + * taint would also satisfy. These controls run the identical config with the cleaner REMOVED and + * demand the finding: a red control means its silent sibling proves nothing. + */ + private fun noCleanerConfig( + entryPointMethod: String, + sinkMethod: String, + source: SerializedRule.EntryPoint, + ) = SerializedTaintConfig( + entryPoint = listOf(source), + cleaner = emptyList(), + sink = listOf(sinkRule(TEST_CLS, sinkMethod, RULE_ID, listOf(Argument(0) to TAINT_MARK))) + ) + + @Test + open fun `non-vacuity - base source reaches the sanitized field with no cleaner`() = assertReachable( + config = noCleanerConfig("boxCleanedFlow", "sinkBox", source("boxCleanedFlow", baseOnly())), + testCls = TEST_CLS, + entryPointName = "boxCleanedFlow", + ruleId = RULE_ID, + testName = "non-vacuity, base source depth 1" + ) + + @Test + open fun `non-vacuity - whole-object source reaches the sanitized field with no cleaner`() = assertReachable( + config = noCleanerConfig( + "boxCleanedFlow", + "sinkBox", + source("boxCleanedFlow", baseOnly(), withModifiers(PositionModifier.AnyField)) + ), + testCls = TEST_CLS, + entryPointName = "boxCleanedFlow", + ruleId = RULE_ID, + testName = "non-vacuity, whole-object source depth 1" + ) + + @Test + open fun `non-vacuity - field source reaches the sanitized field with no cleaner`() = assertReachable( + config = noCleanerConfig("cleanedFlow", "sink", source("cleanedFlow", withModifiers(boxF))), + testCls = TEST_CLS, + entryPointName = "cleanedFlow", + ruleId = RULE_ID, + testName = "non-vacuity, field source depth 2" + ) + + @Test + open fun `non-vacuity - any-field source reaches the sanitized field with no cleaner`() = assertReachable( + config = noCleanerConfig( + "nodeCleanedFlow", + "sink", + source("nodeCleanedFlow", withModifiers(PositionModifier.AnyField)) + ), + testCls = TEST_CLS, + entryPointName = "nodeCleanedFlow", + ruleId = RULE_ID, + testName = "non-vacuity, any-field source depth 3" + ) +} + +/** + * Pins the tree representation separately: all four non-vacuity controls and the corresponding + * deep-clean cases must remain measurable here, including the deep starred reads. + */ +class TreeCleanerFieldSensitivityAnalysisTest : CleanerFieldSensitivityAnalysisTest() + +class AutomataCleanerFieldSensitivityAnalysisTest : CleanerFieldSensitivityAnalysisTest() { + override val apMode: ApMode = ApMode.Automata +} diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/DeepCleanSummaryAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/DeepCleanSummaryAnalysisTest.kt new file mode 100644 index 000000000..61eb14e7e --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/DeepCleanSummaryAnalysisTest.kt @@ -0,0 +1,256 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.ApMode +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.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintCleanAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +/** + * A whole-object (`$*`) sanitizer inside a summarized wrapper must stay effective when the + * wrapper ALSO has an unsanitized flow from the same initial fact. + * + * `wrap` copies its whole argument twice — once before the starred clean (`p.raw`) and once after + * it (`p.val`). Both summary edges share the initial fact `b`; its AnyField mark exclusion must stay + * attached to the sanitized branch while the unsanitized branch remains reported. + * + * Both Tree and Automata exercise the same AnyField mark-exclusion contract, including transport + * through an identity summary. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class DeepCleanSummaryAnalysisTest : AnalysisTest() { + + companion object { + private const val TEST_CLS = "test.samples.DeepCleanSummarySample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "deep-clean-summary-rule" + } + + override val sourceFileExtension: String = "java" + + override val analysisUnrollStrategy: AnyAccessorUnrollStrategy = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + + private fun wholeObjectEntryPoint(entryPointMethod: String) = SerializedRule.EntryPoint( + function = functionMatcher(TEST_CLS, entryPointMethod), + taint = listOf( + SerializedTaintAssignAction( + kind = TAINT_MARK, + pos = PositionBaseWithModifiers.BaseOnly(Argument(0)) + ), + SerializedTaintAssignAction( + kind = TAINT_MARK, + pos = PositionBaseWithModifiers.WithModifiers(Argument(0), listOf(PositionModifier.AnyField)) + ) + ) + ) + + private fun starredCleaner(function: String = "clean") = SerializedRule.Cleaner( + function = functionMatcher(TEST_CLS, function), + cleans = listOf( + SerializedTaintCleanAction( + taintKind = TAINT_MARK, + pos = PositionBaseWithModifiers.BaseOnly(Argument(0)) + ), + SerializedTaintCleanAction( + taintKind = TAINT_MARK, + pos = PositionBaseWithModifiers.WithModifiers(Argument(0), listOf(PositionModifier.AnyField)) + ) + ) + ) + + private fun baseOnlyCleaner() = SerializedRule.Cleaner( + function = functionMatcher(TEST_CLS, "clean"), + cleans = listOf( + SerializedTaintCleanAction( + taintKind = TAINT_MARK, + pos = PositionBaseWithModifiers.BaseOnly(Argument(0)) + ) + ) + ) + + private fun config(entryPointMethod: String) = SerializedTaintConfig( + entryPoint = listOf(wholeObjectEntryPoint(entryPointMethod)), + cleaner = listOf(starredCleaner()), + sink = listOf(sinkRule(TEST_CLS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))) + ) + + private fun baseOnlyCleanConfig(entryPointMethod: String) = SerializedTaintConfig( + entryPoint = listOf(wholeObjectEntryPoint(entryPointMethod)), + cleaner = listOf(baseOnlyCleaner()), + sink = listOf(sinkRule(TEST_CLS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))) + ) + + private fun anyFieldOnlyEntryPoint(entryPointMethod: String) = SerializedRule.EntryPoint( + function = functionMatcher(TEST_CLS, entryPointMethod), + taint = listOf( + SerializedTaintAssignAction( + kind = TAINT_MARK, + pos = PositionBaseWithModifiers.WithModifiers(Argument(0), listOf(PositionModifier.AnyField)) + ) + ) + ) + + private fun anyFieldOnlyConfig(entryPointMethod: String) = SerializedTaintConfig( + entryPoint = listOf(anyFieldOnlyEntryPoint(entryPointMethod)), + cleaner = listOf(starredCleaner()), + sink = listOf(sinkRule(TEST_CLS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))) + ) + + @Test + fun `any-field-only taint - unsanitized sibling edge stays reported`() { + // No base mark: the whole transport rides the abstract-initial summary edges, so a + // storage that smears the sanitized sibling's exclusions onto the unsanitized edge + // loses this finding. + assertReachable( + config = anyFieldOnlyConfig("uncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "uncleanedFlow", + ruleId = RULE_ID, + testName = "any-field-only uncleaned sibling flow" + ) + } + + @Test + open fun `any-field-only taint - sanitized sibling edge stays clean`() { + assertNotReachable( + config = anyFieldOnlyConfig("cleanedFlow"), + testCls = TEST_CLS, + entryPointName = "cleanedFlow", + testName = "any-field-only cleaned sibling flow" + ) + } + + @Test + fun `branch-conditional clean keeps the unsanitized path reported`() { + // One branch cleans, the other does not: the unsanitized path must stay reported. + // PROBE for reviewer's concern: intra-method edge storages union one exclusion slot + // per (initial AP, statement) across alternatives, smearing the sanitizer's exclusions + // onto the unsanitized branch at the join. + assertReachable( + config = config("conditionalCleanFlow"), + testCls = TEST_CLS, + entryPointName = "conditionalCleanFlow", + ruleId = RULE_ID, + testName = "branch-conditional clean flow" + ) + } + + @Test + fun `base-only clean keeps the whole-object field taint through the wrapper`() { + // The clean removes only the base-value mark; the any-field mark survives the call + // and the sanitized-side read stays tainted. This pins cleaner-state transport: + // the fact enters the resolved `clean` via call-to-start and must + // re-emerge from its identity summary. + assertReachable( + config = baseOnlyCleanConfig("cleanedFlow"), + testCls = TEST_CLS, + entryPointName = "cleanedFlow", + ruleId = RULE_ID, + testName = "base-only clean continuation flow" + ) + } + + @Test + fun `starred clean in a summarized wrapper with only the cleaned flow`() { + assertNotReachable( + config = config("cleanOnlyFlow"), + testCls = TEST_CLS, + entryPointName = "cleanOnlyFlow", + testName = "clean-only wrapper flow" + ) + } + + @Test + open fun `starred clean survives an unsanitized sibling edge from the same initial fact`() { + assertNotReachable( + config = config("cleanedFlow"), + testCls = TEST_CLS, + entryPointName = "cleanedFlow", + testName = "cleaned sibling flow" + ) + } + + @Test + fun `unsanitized sibling edge from the same initial fact stays reported`() { + assertReachable( + config = config("uncleanedFlow"), + testCls = TEST_CLS, + entryPointName = "uncleanedFlow", + ruleId = RULE_ID, + testName = "uncleaned sibling flow" + ) + } + + // The clean and the read inside ONE summarized helper: the claim must be effective within + // the helper's own summary computation, not only after transit to the caller's fact. + + @Test + open fun `in-helper starred clean silences the read in the same summary`() { + assertNotReachable( + config = config("helperCleanReadFlow"), + testCls = TEST_CLS, + entryPointName = "helperCleanReadFlow", + testName = "in-helper clean-then-read flow" + ) + } + + @Test + open fun `in-helper read without a clean stays reported`() { + assertReachable( + config = config("helperReadFlow"), + testCls = TEST_CLS, + entryPointName = "helperReadFlow", + ruleId = RULE_ID, + testName = "in-helper read control" + ) + } + + @Test + open fun `in-helper nested starred clean silences the read`() { + assertNotReachable( + config = config("helperNestedCleanReadFlow"), + testCls = TEST_CLS, + entryPointName = "helperNestedCleanReadFlow", + testName = "in-helper nested clean-then-read flow" + ) + } + + @Test + open fun `clean plus depth-2 constant store returns a silent object`() { + val nodeConfig = SerializedTaintConfig( + entryPoint = listOf(wholeObjectEntryPoint("nodeCleanAssignFlow")), + cleaner = listOf(starredCleaner("cleanNode")), + sink = listOf(sinkRule(TEST_CLS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))) + ) + assertNotReachable( + config = nodeConfig, + testCls = TEST_CLS, + entryPointName = "nodeCleanAssignFlow", + testName = "clean-then-assign leaf flow" + ) + } +} + +/** + * The sibling cases pass precisely: `wrap` stores its argument into `p.raw` before the starred + * clean and into `p.val` after it, and the clean's residual claim rides the exit tree's `.val` + * abstraction (AnyFieldMarkExclusions) without ever meeting `.raw`. The unsanitized sibling stays + * reported, the sanitized one stays silent. + */ +class TreeDeepCleanSummaryAnalysisTest : DeepCleanSummaryAnalysisTest() + +class AutomataDeepCleanSummaryAnalysisTest : DeepCleanSummaryAnalysisTest() { + override val apMode: ApMode = ApMode.Automata +}