diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluator.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluator.kt new file mode 100644 index 0000000000..7d2654dba3 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluator.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.value.* +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.statements.DoStatement +import de.fraunhofer.aisec.cpg.graph.statements.ForEachStatement +import de.fraunhofer.aisec.cpg.graph.statements.ForStatement +import de.fraunhofer.aisec.cpg.graph.statements.WhileStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* +import de.fraunhofer.aisec.cpg.helpers.* +import kotlin.reflect.KClass +import kotlin.reflect.full.createInstance +import org.apache.commons.lang3.NotImplementedException + +/** + * An evaluator performing abstract evaluation for a singular [Value]. It takes a target [Node] and + * walks back to its Declaration. From there it uses the [Worklist] to traverse the EOG graph until + * it reaches the node. All statements encountered may influence the result as implemented in the + * respective [Value] class. The result is a [LatticeInterval] defining both a lower and upper bound + * for the final value. + */ +class AbstractEvaluator { + // The node for which we want to get the value + private lateinit var targetNode: Node + // The name of the value we are analyzing + private lateinit var targetName: String + // The type of the value we are analyzing + private lateinit var targetType: KClass + + /** + * Takes a node (e.g. Reference) and tries to evaluate its value at this point in the program. + */ + fun evaluate(node: Node): LatticeInterval { + return evaluate( + node.name.localName, + getInitializerOf(node)!!, + node, + getType(node), + IntervalLattice(LatticeInterval.BOTTOM) + ) + } + + /** + * Takes a manual configuration and tries to evaluate the value of the node at the end. + * + * @param name The name of the target node + * @param start The beginning of the analysis, usually the start of the target's life + * @param end The place at which we want to know the target's value + * @param type The Type of the target + * @param interval The starting value of the analysis, optional + */ + fun evaluate( + name: String, + start: Node, + end: Node, + type: KClass, + interval: IntervalLattice? = null + ): LatticeInterval { + targetNode = end + targetName = name + targetType = type + + // evaluate effect of each operation on the list until we reach "node" + val startState = IntervalState() + startState.push(start, interval) + val finalState = iterateEOG(start, startState, ::handleNode, targetNode) + return finalState?.get(targetNode)?.elements ?: LatticeInterval.BOTTOM + } + + /** + * This function changes the state depending on the current node. This is the handler used in + * _iterateEOG_ to correctly handle complex statements. + * + * @param currentNode The current node + * @param state The state for the current node + * @param worklist The whole worklist to manually handle complex scenarios + * @return The updated state after handling the current node + */ + private fun handleNode( + currentNode: Node, + state: State, + worklist: Worklist + ): State { + // Check if the current node is already DONE + // (prevents infinite loop and unnecessary double-checking) + if (worklist.isDone(currentNode)) { + // Mark following nodes as DONE if they only have this as previousEOG or all are DONE + // In other cases, converging branches may still change the node + currentNode.nextEOG.forEach { next -> + if ( + next.prevEOG.singleOrNull() == currentNode || + next.prevEOG.all { worklist.isDone(it) } + ) { + worklist.evaluationStateMap[next] = Worklist.EvaluationState.DONE + } + } + return state + } + + // Calculate the effect of the currentNode + val previousInterval = state[currentNode]?.elements + val newInterval = state.calculateEffect(currentNode) + val newState = state.duplicate() + + // Check if it is marked as in need of widening + if (worklist.needsWidening(currentNode)) { + // Widen the interval + val widenedInterval = previousInterval!!.widen(newInterval) + // Check if the widening caused a change + if (widenedInterval != previousInterval) { + // YES: mark next nodes as needs widening, add them to worklist + // Overwrite current interval, mark this node as needs narrowing + newState[currentNode] = IntervalLattice(widenedInterval) + currentNode.nextEOG.forEach { + if (!isLoopEnd(it)) { + worklist.evaluationStateMap[it] = Worklist.EvaluationState.WIDENING + } + } + worklist.evaluationStateMap[currentNode] = Worklist.EvaluationState.NARROWING + } else { + // NO: mark the current node as DONE + // We never mark loop heads as DONE as they prevent loop entering otherwise + if (!isLoopHead(currentNode)) { + worklist.evaluationStateMap[currentNode] = Worklist.EvaluationState.DONE + } + } + } + + // Otherwise, check if it is marked as in need of narrowing + else if (worklist.needsNarrowing(currentNode)) { + // Narrow the interval + val narrowedInterval = previousInterval!!.narrow(newInterval) + // Check if the narrowing caused a change + if (narrowedInterval != previousInterval) { + // YES: overwrite and keep this node marked + // Mark next nodes as need narrowing and add to worklist + newState[currentNode] = IntervalLattice(narrowedInterval) + currentNode.nextEOG.forEach { + worklist.evaluationStateMap[it] = Worklist.EvaluationState.NARROWING + } + } else { + // NO: mark the node as DONE + // We never mark loop heads as DONE as they prevent loop entering otherwise + if (!isLoopHead(currentNode)) { + worklist.evaluationStateMap[currentNode] = Worklist.EvaluationState.DONE + } + } + } + + // Otherwise, if it was seen for the first time directly apply the effect. + // If it is within a loop mark it as "NEEDS WIDENING". + // We cannot use the "already_seen" field as it is set before this handler is called + else { + newState[currentNode] = IntervalLattice(newInterval) + // We mark the node as needs widening if it is either a loop head or any previous node + // is marked, but not if the current node ends a loop + if ( + isLoopHead(currentNode) || + (currentNode.prevEOG.any { worklist.needsWidening(it) } && + !isLoopEnd(currentNode)) + ) { + worklist.evaluationStateMap[currentNode] = Worklist.EvaluationState.WIDENING + } + } + + // Finally, we propagate the current Interval to all successor nodes which are empty. + // If the next EOG already has a value we need to join them. + // This is implemented in IntervalState.push. + // Only do this if we have not yet reached the goal node + if (currentNode != targetNode) { + currentNode.nextEOG.forEach { newState.push(it, newState[currentNode]) } + } + + return newState + } + + private fun getInitializerOf(node: Node): Node? { + return Value.getInitializer(node) + } + + private fun State.calculateEffect(node: Node): LatticeInterval { + return targetType.createInstance().applyEffect(this[node]!!.elements, node, targetName) + } + + /** + * Tries to determine the type of the target Node by parsing the type name. + * + * @param node The target node + * @return A [Value] class that models the effects on the node type + */ + private fun getType(node: Node): KClass { + if (node !is Reference) { + throw NotImplementedException() + } + val name = node.type.name.toString() + return when { + name.endsWith("[]") -> ArrayValue::class + name == "int" -> IntegerValue::class + else -> throw NotImplementedException() + } + } + + /** This method checks for known loop heads such as For, While or Do. */ + private fun isLoopHead(node: Node): Boolean { + return when (node) { + is ForStatement, + is WhileStatement, + is ForEachStatement, + is DoStatement -> true + else -> false + } + } + + /** + * This method checks if the current node marks the end of a loop. It assumes that nextEOG[1] of + * the loop head always points to the loop end. + */ + private fun isLoopEnd(node: Node): Boolean { + val head = node.prevEOG.firstOrNull { isLoopHead(it) } ?: return false + return head.nextEOG[1] == node + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeInterval.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeInterval.kt new file mode 100644 index 0000000000..8a3aadc0c8 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeInterval.kt @@ -0,0 +1,496 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval + +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.helpers.LatticeElement +import de.fraunhofer.aisec.cpg.helpers.State + +/** + * The [LatticeInterval] class implements the functionality of intervals that is needed for the + * [AbstractEvaluator]. It is either a [BOTTOM] object signaling no knowledge or a [Bounded] object + * with a lower and upper [Bound]. Each [Bound] can then be [Bound.NEGATIVE_INFINITE], + * [Bound.INFINITE] or a [Bound.Value]. This class implements many convenience methods to handle the + * [LatticeInterval]. + */ +sealed class LatticeInterval : Comparable { + object BOTTOM : LatticeInterval() + + class Bounded(arg1: Bound, arg2: Bound) : LatticeInterval() { + val lower: Bound + val upper: Bound + + constructor(arg1: Int, arg2: Int) : this(Bound.Value(arg1), Bound.Value(arg2)) + + constructor(arg1: Int, arg2: Bound) : this(Bound.Value(arg1), arg2) + + constructor(arg1: Bound, arg2: Int) : this(arg1, Bound.Value(arg2)) + + // Automatically switch the arguments if the upper bound is lower than the lower bound + init { + if (arg1 > arg2) { + lower = arg2 + upper = arg1 + } else { + lower = arg1 + upper = arg2 + } + } + + override fun toString(): String { + return "[$lower, $upper]" + } + } + + // TODO: future iterations should support fractional values + sealed class Bound : Comparable { + data class Value(val value: Int) : Bound() { + override fun toString(): String { + return value.toString() + } + } + + // necessary values for widening and narrowing + data object NEGATIVE_INFINITE : Bound() + + data object INFINITE : Bound() + + override fun compareTo(other: Bound): Int { + return when { + this is NEGATIVE_INFINITE && other !is NEGATIVE_INFINITE -> -1 + this is INFINITE && other !is INFINITE -> 1 + other is NEGATIVE_INFINITE && this !is NEGATIVE_INFINITE -> 1 + other is INFINITE && this !is INFINITE -> -1 + this is Value && other is Value -> this.value.compareTo(other.value) + else -> 0 + } + } + + override fun toString(): String { + return when (this) { + is Value -> value.toString() + is INFINITE -> "INFINITE" + is NEGATIVE_INFINITE -> "NEGATIVE_INFINITE" + } + } + } + + // Comparing two Intervals. They are treated as equal if they overlap + // BOTTOM intervals are considered "smaller" than known intervals + override fun compareTo(other: LatticeInterval): Int { + return when { + this is BOTTOM && other !is BOTTOM -> -1 + other is BOTTOM && this !is BOTTOM -> 1 + this is Bounded && other is Bounded -> { + when { + this.lower > other.upper -> 1 + this.upper < other.lower -> -1 + else -> 0 + } + } + else -> 0 + } + } + + // Equals check only true if both Intervals are true or have the same boundaries + // Not the same as a zero result in compareTo! + override fun equals(other: Any?): Boolean { + return when (other) { + !is LatticeInterval -> return false + is BOTTOM -> this is BOTTOM + is Bounded -> { + when (this) { + is Bounded -> { + this.lower == other.lower && this.upper == other.upper + } + else -> false + } + } + else -> false + } + } + + // Addition operator + operator fun plus(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = addBounds(this.lower, other.lower) + val newUpper = addBounds(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + // Subtraction operator + operator fun minus(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = subtractBounds(this.lower, other.lower) + val newUpper = subtractBounds(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + // Multiplication operator + operator fun times(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = multiplyBounds(this.lower, other.lower) + val newUpper = multiplyBounds(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + // Division operator + operator fun div(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = divideBounds(this.lower, other.lower) + val newUpper = divideBounds(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + // Modulo operator + operator fun rem(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val lowerBracket = modulateBounds(this.lower, other.lower) + val upperBracket = modulateBounds(this.upper, other.upper) + lowerBracket.join(upperBracket) + } + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + // Join Operation + fun join(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = min(this.lower, other.lower) + val newUpper = max(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + // Meet Operation + fun meet(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM -> other + other is BOTTOM -> this + // Check if the overlap at all + this.compareTo(other) != 0 -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = max(this.lower, other.lower) + val newUpper = min(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + // Widening + fun widen(other: LatticeInterval): LatticeInterval { + if (this !is Bounded) { + return other + } else if (other !is Bounded) { + return this + } + val lower: Bound = + when { + max(this.lower, other.lower) == other.lower -> { + this.lower + } + else -> Bound.NEGATIVE_INFINITE + } + val upper: Bound = + when { + max(this.upper, other.upper) == this.upper -> { + this.upper + } + else -> Bound.INFINITE + } + return Bounded(lower, upper) + } + + // Narrowing + fun narrow(other: LatticeInterval): LatticeInterval { + if (this !is Bounded || other !is Bounded) { + return BOTTOM + } + val lower: Bound = + when { + this.lower == Bound.NEGATIVE_INFINITE -> { + other.lower + } + else -> this.lower + } + val upper: Bound = + when { + this.upper == Bound.INFINITE -> { + other.upper + } + else -> this.upper + } + return Bounded(lower, upper) + } + + private fun min(one: Bound, other: Bound): Bound { + return when { + one is Bound.INFINITE || other is Bound.NEGATIVE_INFINITE -> other + other is Bound.INFINITE || one is Bound.NEGATIVE_INFINITE -> one + one is Bound.Value && other is Bound.Value -> + Bound.Value(kotlin.math.min(one.value, other.value)) + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + private fun max(one: Bound, other: Bound): Bound { + return when { + one is Bound.INFINITE || other is Bound.NEGATIVE_INFINITE -> one + other is Bound.INFINITE || one is Bound.NEGATIVE_INFINITE -> other + one is Bound.Value && other is Bound.Value -> + Bound.Value(kotlin.math.max(one.value, other.value)) + else -> throw IllegalArgumentException("Unsupported interval type") + } + } + + private fun addBounds(a: Bound, b: Bound): Bound { + return when { + // -∞ + ∞ is not a defined operation + a is Bound.INFINITE && b !is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + a is Bound.NEGATIVE_INFINITE && b !is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + b is Bound.INFINITE && a !is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + b is Bound.NEGATIVE_INFINITE && a !is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + a is Bound.Value && b is Bound.Value -> Bound.Value(a.value + b.value) + else -> throw IllegalArgumentException("Unsupported bound type") + } + } + + private fun subtractBounds(a: Bound, b: Bound): Bound { + return when { + // ∞ - ∞ is not a defined operation + a is Bound.INFINITE && b !is Bound.INFINITE -> Bound.INFINITE + a is Bound.NEGATIVE_INFINITE && b !is Bound.NEGATIVE_INFINITE -> Bound.NEGATIVE_INFINITE + b is Bound.INFINITE && a !is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + b is Bound.NEGATIVE_INFINITE && a !is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + a is Bound.Value && b is Bound.Value -> Bound.Value(a.value - b.value) + else -> throw IllegalArgumentException("Unsupported bound type") + } + } + + private fun multiplyBounds(a: Bound, b: Bound): Bound { + return when { + // ∞ * 0 is not a defined operation + a is Bound.INFINITE && b > Bound.Value(0) -> Bound.INFINITE + a is Bound.INFINITE && b < Bound.Value(0) -> Bound.NEGATIVE_INFINITE + a > Bound.Value(0) && b is Bound.INFINITE -> Bound.INFINITE + a < Bound.Value(0) && b is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b > Bound.Value(0) -> Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b < Bound.Value(0) -> Bound.INFINITE + a > Bound.Value(0) && b is Bound.NEGATIVE_INFINITE -> Bound.NEGATIVE_INFINITE + a < Bound.Value(0) && b is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + a is Bound.Value && b is Bound.Value -> Bound.Value(a.value * b.value) + else -> throw IllegalArgumentException("Unsupported bound type") + } + } + + private fun divideBounds(a: Bound, b: Bound): Bound { + return when { + // ∞ / ∞ is not a defined operation + // x / 0 is not a defined operation + a is Bound.INFINITE && b > Bound.Value(0) && b !is Bound.INFINITE -> Bound.INFINITE + a is Bound.INFINITE && b < Bound.Value(0) && b !is Bound.NEGATIVE_INFINITE -> + Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b > Bound.Value(0) && b !is Bound.INFINITE -> + Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b < Bound.Value(0) && b !is Bound.NEGATIVE_INFINITE -> + Bound.INFINITE + // We estimate x / ∞ as 0 (with x != ∞) + a !is Bound.NEGATIVE_INFINITE && + a !is Bound.INFINITE && + (b is Bound.NEGATIVE_INFINITE || b is Bound.INFINITE) -> Bound.Value(0) + a is Bound.Value && b is Bound.Value && b != Bound.Value(0) -> + Bound.Value(a.value / b.value) + else -> throw IllegalArgumentException("Unsupported bound type") + } + } + + // ∞ mod b can be any number [0, b], therefore we need to return an Interval + private fun modulateBounds(a: Bound, b: Bound): LatticeInterval { + return when { + // x mod 0 is not a defined operation + // we approximate ∞ mod x as any number [0, b] (with x != ∞) + // x mod -∞ is not a defined operation + a == Bound.Value(0) -> Bounded(0, 0) + (a is Bound.INFINITE || a is Bound.NEGATIVE_INFINITE) && b != Bound.Value(0) -> + Bounded(0, b) + b is Bound.INFINITE -> Bounded(a, a) + a is Bound.Value && b is Bound.Value && b != Bound.Value(0) -> + Bounded(a.value % b.value, a.value % b.value) + else -> throw IllegalArgumentException("Unsupported bound type") + } + } + + override fun toString(): String { + return when (this) { + is BOTTOM -> "BOTTOM" + is Bounded -> this.toString() + } + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } +} + +/** + * The [LatticeElement] that is used for worklist iteration. It wraps a single element of the type + * [LatticeInterval]. + */ +class IntervalLattice(override val elements: LatticeInterval) : + LatticeElement(elements) { + override fun compareTo(other: LatticeElement): Int { + return elements.compareTo(other.elements) + } + + override fun equals(other: Any?): Boolean { + if (other !is IntervalLattice) { + return false + } + return this.elements == other.elements + } + + /** Returns true iff [other] is fully within this */ + fun contains(other: LatticeElement): Boolean { + if (this.elements is LatticeInterval.BOTTOM || other.elements is LatticeInterval.BOTTOM) { + return false + } + val thisInterval = this.elements as LatticeInterval.Bounded + val otherInterval = other.elements as LatticeInterval.Bounded + + return (thisInterval.lower <= otherInterval.lower && + thisInterval.upper >= otherInterval.upper) + } + + /** The least upper bound of two Intervals is given by the join operation. */ + override fun lub(other: LatticeElement): LatticeElement { + return IntervalLattice(this.elements.join(other.elements)) + } + + fun widen(other: IntervalLattice): IntervalLattice { + return IntervalLattice(this.elements.widen(other.elements)) + } + + fun narrow(other: IntervalLattice): IntervalLattice { + return IntervalLattice(this.elements.narrow(other.elements)) + } + + override fun duplicate(): LatticeElement { + return when { + elements is LatticeInterval.Bounded -> + IntervalLattice(LatticeInterval.Bounded(elements.lower, elements.upper)) + else -> IntervalLattice(LatticeInterval.BOTTOM) + } + } +} + +/** + * A [State] that maps analyzed [Node]s to their [LatticeInterval]. Whenever new information for a + * known node is pushed, we join it with the previous known value to properly handle branch merges. + */ +class IntervalState : State() { + /** + * Adds a new mapping from [newNode] to (a copy of) [newLatticeElement] to this object if + * [newNode] does not exist in this state yet. If it already exists, it will compute the [lub] + * over the new [LatticeElement] and the previous one. It returns whether the [LatticeElement] + * has changed. + */ + override fun push( + newNode: de.fraunhofer.aisec.cpg.graph.Node, + newLatticeElement: LatticeElement? + ): Boolean { + if (newLatticeElement == null) { + return false + } + val current = this[newNode] + if (current != null) { + // Calculate the join of the new Element and the previous (propagated) value for the + // node + val joinedElement = current.lub(newLatticeElement) + // Use the joinedElement if it differs from before + if (joinedElement != this[newNode]) { + this[newNode] = joinedElement + return true + } + return false + } else { + this[newNode] = newLatticeElement + } + return true + } + + /** + * Implements the same duplication as the parent function, but returns a [IntervalState] object + * instead. + */ + override fun duplicate(): State { + val clone = IntervalState() + for ((key, value) in this) { + clone[key] = value.duplicate() + } + return clone + } + + /** + * Implements the same [lub] function as the parent, but uses the [push] function from + * [LatticeInterval] + */ + override fun lub( + other: State + ): Pair, Boolean> { + var update = false + for ((node, newLattice) in other) { + update = push(node, newLattice) || update + } + return Pair(this, update) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValue.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValue.kt new file mode 100644 index 0000000000..1f8739de95 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValue.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.InitializerListExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewArrayExpression +import de.fraunhofer.aisec.cpg.query.value +import org.apache.commons.lang3.NotImplementedException + +/** + * This class implements the [Value] interface for Arrays, tracking the size of the collection. We + * assume that there is no operation that changes an array's size apart from re-declaring it. + */ +class ArrayValue : Value { + override fun applyEffect(current: LatticeInterval, node: Node, name: String): LatticeInterval { + // (Re-)Declaration + if ( + node is VariableDeclaration && node.initializer != null && node.name.localName == name + ) { + val initValue = getSize(node.initializer!!) + return LatticeInterval.Bounded(initValue, initValue) + } + return current + } + + private fun getSize(node: Node): Int { + return when (node) { + // TODO: depending on the desired behavior we could distinguish between included types + // (e.g. String and Int Literals) + is Literal<*> -> { + 1 + } + is InitializerListExpression -> { + node.initializers.fold(0) { acc, init -> acc + getSize(init) } + } + is NewArrayExpression -> { + if (node.initializer != null) { + getSize(node.initializer!!) + } else { + node.dimensions + .map { it.value.value as Int } + .reduce { acc, dimension -> acc * dimension } + } + } + else -> throw NotImplementedException() + } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValue.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValue.kt new file mode 100644 index 0000000000..5752615870 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValue.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator +import org.apache.commons.lang3.NotImplementedException + +/** This class implements the [Value] interface for Integer values. */ +class IntegerValue : Value { + override fun applyEffect(current: LatticeInterval, node: Node, name: String): LatticeInterval { + // (Re-)Declarations of the Variable + if ( + node is VariableDeclaration && node.initializer != null && node.name.localName == name + ) { + val initValue = + when (val init = node.initializer) { + is Literal<*> -> init.value as? Int ?: throw NotImplementedException() + else -> throw NotImplementedException() + } + return LatticeInterval.Bounded(initValue, initValue) + } + // Unary Operators + if (node is UnaryOperator) { + if (node.input.code == name) { + return when (node.operatorCode) { + "++" -> { + val oneInterval = LatticeInterval.Bounded(1, 1) + current + oneInterval + } + "--" -> { + val oneInterval = LatticeInterval.Bounded(1, 1) + current - oneInterval + } + else -> current + } + } + } + // Assignments and combined assign expressions + // TODO: we should aim to evaluate the right hand side expression for all cases! + // currently evaluation only works correctly for literals + else if (node is AssignExpression) { + if (node.lhs.any { it.code == name }) { + return when (node.operatorCode) { + "=" -> { + // If the rhs is only a literal use this exact value + val value = (node.rhs.getOrNull(0) as? Literal<*>)?.value as? Int + val newInterval = + if (value != null) { + LatticeInterval.Bounded(value, value) + } + // Per default set the bounds to unknown + else { + LatticeInterval.Bounded( + LatticeInterval.Bound.NEGATIVE_INFINITE, + LatticeInterval.Bound.INFINITE + ) + } + newInterval + } + "+=" -> { + // If the rhs is only a literal we subtract this exact value + val value = (node.rhs.getOrNull(0) as? Literal<*>)?.value as? Int + val newInterval = + if (value != null) { + val valueInterval = LatticeInterval.Bounded(value, value) + current + valueInterval + } + // Per default lose all information + else { + val joinInterval: LatticeInterval = + LatticeInterval.Bounded( + LatticeInterval.Bound.NEGATIVE_INFINITE, + LatticeInterval.Bound.INFINITE + ) + current.join(joinInterval) + } + newInterval + } + "-=" -> { + // If the rhs is only a literal we subtract this exact value + val value = (node.rhs.getOrNull(0) as? Literal<*>)?.value as? Int + val newInterval = + if (value != null) { + val valueInterval = LatticeInterval.Bounded(value, value) + current - valueInterval + } + // Per default lose all information + else { + val joinInterval: LatticeInterval = + LatticeInterval.Bounded( + LatticeInterval.Bound.NEGATIVE_INFINITE, + LatticeInterval.Bound.INFINITE + ) + current.join(joinInterval) + } + newInterval + } + "*=" -> { + // If the rhs is only a literal we subtract this exact value + val value = (node.rhs.getOrNull(0) as? Literal<*>)?.value as? Int + val newInterval = + if (value != null) { + val valueInterval = LatticeInterval.Bounded(value, value) + current * valueInterval + } + // Per default lose all information + else { + LatticeInterval.Bounded( + LatticeInterval.Bound.NEGATIVE_INFINITE, + LatticeInterval.Bound.INFINITE + ) + } + newInterval + } + "/=" -> { + // If the rhs is only a literal we subtract this exact value + val value = (node.rhs.getOrNull(0) as? Literal<*>)?.value as? Int + val newInterval = + if (value != null) { + val valueInterval = LatticeInterval.Bounded(value, value) + current / valueInterval + } + // Per default lose all information + else { + LatticeInterval.Bounded( + LatticeInterval.Bound.NEGATIVE_INFINITE, + LatticeInterval.Bound.INFINITE + ) + } + newInterval + } + "%=" -> { + // If the rhs is only a literal we subtract this exact value + val value = (node.rhs.getOrNull(0) as? Literal<*>)?.value as? Int + val newInterval = + if (value != null) { + val valueInterval = LatticeInterval.Bounded(value, value) + current % valueInterval + } + // Per default lose all information + else { + LatticeInterval.Bounded( + LatticeInterval.Bound.NEGATIVE_INFINITE, + LatticeInterval.Bound.INFINITE + ) + } + newInterval + } + else -> current + } + } + } + return current + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValue.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValue.kt new file mode 100644 index 0000000000..d0a255c5fb --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValue.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewExpression +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import org.apache.commons.lang3.NotImplementedException + +/** + * This class implements the [Value] interface for Mutable Lists, tracking the size of the + * collection. We assume that there is no operation that changes an array's size apart from + * re-declaring it. NOTE: This is an unpolished example implementation. Before actual usage consider + * the below TODOs and write a test file. + */ +@Suppress("UNUSED") +class MutableListValue : Value { + override fun applyEffect(current: LatticeInterval, node: Node, name: String): LatticeInterval { + if ( + node is VariableDeclaration && node.initializer != null && node.name.localName == name + ) { + when (val init = node.initializer) { + is MemberCallExpression -> { + val size = init.arguments.size + return LatticeInterval.Bounded(size, size) + } + is NewExpression -> { + // TODO: consider collection as argument! + return LatticeInterval.Bounded(0, 0) + } + else -> throw NotImplementedException() + } + } + // State can only be directly changed via MemberCalls (add, clear, ...) + if (node !is MemberCallExpression) { + return current + } + // If the call does not have the subject as base, check for subject as argument + if ((node.callee as? MemberExpression)?.base?.code != name) { + if (node.arguments.any { it.name.localName == name }) { + TODO() + // This is a function call that uses the variable as an argument. + // To find side effects we need to create a local evaluator for this function + // and return the value of the renamed variable at the last statement. + } else { + return current + } + } + return when (node.name.localName) { + "add" -> { + val oneInterval = LatticeInterval.Bounded(1, 1) + current + oneInterval + } + // TODO: this should trigger another List size evaluation for the argument! + // also check and prevent -1 result + "addAll" -> { + val openUpper = LatticeInterval.Bounded(0, LatticeInterval.Bound.INFINITE) + current + openUpper + } + "clear" -> { + LatticeInterval.Bounded(0, 0) + } + "remove" -> { + // We have to differentiate between remove with index or object argument + // Latter may do nothing if the element is not in the list + val modificationInterval = + if (node.arguments.first().type is IntegerType) { + LatticeInterval.Bounded(1, 1) + } else { + LatticeInterval.Bounded(1, 0) + } + // This meet makes sure we do not drop below zero + (current - modificationInterval).meet( + LatticeInterval.Bounded(0, LatticeInterval.Bound.INFINITE) + ) + } + // The size of the argument list is (almost) irrelevant as it has no influence on the + // possible outcomes. + // We could check if it is empty, but that causes significant overhead + "removeAll" -> { + val zeroInterval = LatticeInterval.Bounded(0, 0) + current.join(zeroInterval) + } + else -> current + } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/Value.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/Value.kt new file mode 100644 index 0000000000..41c473917a --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/Value.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference + +/** + * The [Value] interface is used by the AbstractEvaluator to store the behaviour of different + * analysis targets. Each class implementing this interface is expected to define all operations + * that might affect its internal value in [applyEffect]. When adding new classes remember to add + * them to AbstractEvaluator.getType and add tests. + */ +interface Value { + /** Applies the effect of a Node to the interval containing its possible values. */ + fun applyEffect(current: LatticeInterval, node: Node, name: String): LatticeInterval + + companion object { + fun getInitializer(node: Node?): Node? { + return when (node) { + null -> null + is Reference -> getInitializer(node.refersTo) + is VariableDeclaration -> node + else -> getInitializer(node.prevDFG.firstOrNull()) + } + } + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluatorTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluatorTest.kt new file mode 100644 index 0000000000..40be65a747 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluatorTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval + +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.bodyOrNull +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.testcases.AbstractEvaluationTests +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AbstractEvaluatorTest { + private lateinit var tu: TranslationUnitDeclaration + + @BeforeAll + fun beforeAll() { + tu = AbstractEvaluationTests.getIntegerExample().components.first().translationUnits.first() + } + + /* + Bar f = new Bar(); + int a = 5; + + a = 5; + a -= 2; + a += 3; + + b.f(a); + */ + @Test + fun testSimpleInteger() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f1 = mainClass.methods["f1"] + assertNotNull(f1) + + val refA = f1.bodyOrNull(5)!!.arguments.first() + assertNotNull(refA) + + val evaluator = AbstractEvaluator() + val value = evaluator.evaluate(refA) + assertEquals(LatticeInterval.Bounded(1, 1), value) + } + + /* + Bar f = new Bar(); + int a = 5; + + a = 3; + a++; + ++a; + a -= 2; + a += 3; + a--; + --a; + a *= 4; + a /= 2; + a %= 3; + + b.f(a); + */ + @Test + fun testIntegerOperations() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f1 = mainClass.methods["f2"] + assertNotNull(f1) + + val refA = f1.bodyOrNull(12)!!.arguments.first() + assertNotNull(refA) + + val evaluator = AbstractEvaluator() + val value = evaluator.evaluate(refA) + assertEquals(LatticeInterval.Bounded(2, 2), value) + } + + /* + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } + + b.f(a); + */ + @Test + fun testBranch1Integer() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f1 = mainClass.methods["f3"] + assertNotNull(f1) + + val refA = f1.bodyOrNull(3)!!.arguments.first() + assertNotNull(refA) + + val evaluator = AbstractEvaluator() + val value = evaluator.evaluate(refA) + assertEquals(LatticeInterval.Bounded(4, 5), value) + } + + /* + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } else { + a = 3; + } + + b.f(a); + */ + @Test + fun testBranch2Integer() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f1 = mainClass.methods["f4"] + assertNotNull(f1) + + val refA = f1.bodyOrNull(3)!!.arguments.first() + assertNotNull(refA) + + val evaluator = AbstractEvaluator() + val value = evaluator.evaluate(refA) + assertEquals(LatticeInterval.Bounded(3, 4), value) + } + + /* + Bar b = new Bar(); + int a = 5; + + for (int i = 0; i < 5; i++) { + a += 1; + } + + b.f(a); + */ + @Test + fun testLoopInteger() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f1 = mainClass.methods["f5"] + assertNotNull(f1) + + val refA = f1.bodyOrNull(5)!!.arguments.first() + assertNotNull(refA) + + val evaluator = AbstractEvaluator() + val value = evaluator.evaluate(refA) + assertEquals(LatticeInterval.Bounded(5, LatticeInterval.Bound.INFINITE), value) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeIntervalTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeIntervalTest.kt new file mode 100644 index 0000000000..f289d41880 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeIntervalTest.kt @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.BOTTOM +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bound.INFINITE +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bound.NEGATIVE_INFINITE +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bounded +import kotlin.test.* +import kotlin.test.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class LatticeIntervalTest { + @Test + fun testCreate() { + assertDoesNotThrow { BOTTOM } + assertDoesNotThrow { Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) } + assertDoesNotThrow { Bounded(INFINITE, INFINITE) } + assertDoesNotThrow { Bounded(NEGATIVE_INFINITE, INFINITE) } + assertDoesNotThrow { Bounded(0, 0) } + assertDoesNotThrow { Bounded(-5, 5) } + + // Test whether the arguments are switched if necessary + assertEquals(Bounded(NEGATIVE_INFINITE, INFINITE), Bounded(INFINITE, NEGATIVE_INFINITE)) + assertEquals(Bounded(-5, 5), Bounded(5, -5)) + } + + @Test + fun testCompare() { + // comparison including BOTTOM + assertEquals(0, BOTTOM.compareTo(BOTTOM)) + assertEquals(-1, BOTTOM.compareTo(Bounded(-1, 1))) + assertEquals(-1, BOTTOM.compareTo(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(1, Bounded(-1, 1).compareTo(BOTTOM)) + assertEquals(1, Bounded(NEGATIVE_INFINITE, INFINITE).compareTo(BOTTOM)) + + // comparison with non-overlapping intervals + assertEquals(-1, Bounded(NEGATIVE_INFINITE, -1).compareTo(Bounded(1, INFINITE))) + assertEquals(1, Bounded(1, INFINITE).compareTo(Bounded(NEGATIVE_INFINITE, -1))) + + // comparison with overlapping intervals + assertEquals(0, Bounded(NEGATIVE_INFINITE, 0).compareTo(Bounded(0, INFINITE))) + assertEquals(0, Bounded(-4, 2).compareTo(Bounded(-2, 4))) + assertEquals(0, Bounded(-5, 5).compareTo(Bounded(1, 3))) + assertEquals(0, Bounded(-3, 1).compareTo(Bounded(-5, 5))) + } + + @Test + fun testEquals() { + // comparison with BOTTOM + assertFalse(BOTTOM.equals(AbstractEvaluator())) + assertFalse(BOTTOM.equals(null)) + assertFalse(BOTTOM.equals(Bounded(0, 0))) + assertFalse(Bounded(0, 0).equals(BOTTOM)) + assertEquals(BOTTOM, BOTTOM) + + // comparison with different Intervals + assertNotEquals(Bounded(NEGATIVE_INFINITE, 0), Bounded(0, INFINITE)) + assertNotEquals(Bounded(0, 10), Bounded(0, 9)) + assertNotEquals(Bounded(0, 10), Bounded(1, 10)) + assertNotEquals(Bounded(0, 10), Bounded(5, 6)) + assertNotEquals(Bounded(0, 9), Bounded(0, 10)) + assertNotEquals(Bounded(1, 10), Bounded(0, 10)) + assertNotEquals(Bounded(5, 6), Bounded(0, 10)) + + // comparison with same Intervals + assertEquals(Bounded(-5, 5), Bounded(-5, 5)) + assertEquals(Bounded(0, 0), Bounded(0, 0)) + assertEquals(Bounded(NEGATIVE_INFINITE, INFINITE), Bounded(NEGATIVE_INFINITE, INFINITE)) + } + + @Test + fun testAddition() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM + Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) + BOTTOM) + assertEquals(BOTTOM, BOTTOM + Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) + BOTTOM) + assertEquals(BOTTOM, BOTTOM + BOTTOM) + + // Without BOTTOM + assertEquals(Bounded(INFINITE, INFINITE), Bounded(INFINITE, INFINITE) + Bounded(-5, 5)) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + Bounded(-5, 5) + ) + assertEquals(Bounded(INFINITE, INFINITE), Bounded(-5, 5) + Bounded(INFINITE, INFINITE)) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(-5, 5) + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + ) + assertEquals(Bounded(-8, 8), Bounded(-5, 5) + Bounded(-3, 3)) + + // Illegal Operations + assertThrows { + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + Bounded(0, INFINITE) + } + assertThrows { + Bounded(INFINITE, INFINITE) + Bounded(NEGATIVE_INFINITE, 0) + } + } + + @Test + fun testSubtraction() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM - Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) - BOTTOM) + assertEquals(BOTTOM, BOTTOM - Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) - BOTTOM) + assertEquals(BOTTOM, BOTTOM - BOTTOM) + + // Without BOTTOM + assertEquals(Bounded(INFINITE, INFINITE), Bounded(INFINITE, INFINITE) - Bounded(-5, 5)) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) - Bounded(-5, 5) + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(-5, 5) - Bounded(INFINITE, INFINITE) + ) + assertEquals( + Bounded(INFINITE, INFINITE), + Bounded(-5, 5) - Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + ) + assertEquals(Bounded(-2, 2), Bounded(-5, 5) - Bounded(-3, 3)) + + // Illegal Operations + assertThrows { Bounded(-10, INFINITE) - Bounded(0, INFINITE) } + assertThrows { + Bounded(NEGATIVE_INFINITE, 10) - Bounded(NEGATIVE_INFINITE, 0) + } + } + + @Test + fun testMultiplication() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM * Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) * BOTTOM) + assertEquals(BOTTOM, BOTTOM * Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) * BOTTOM) + assertEquals(BOTTOM, BOTTOM * BOTTOM) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(INFINITE, INFINITE) * Bounded(-5, 5) + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) * Bounded(-5, 5) + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(-5, 5) * Bounded(INFINITE, INFINITE) + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(-5, 5) * Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + ) + assertEquals(Bounded(15, 15), Bounded(-5, 5) * Bounded(-3, 3)) + + // Illegal Operations + assertThrows { + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) * Bounded(0, INFINITE) + } + assertThrows { + Bounded(INFINITE, INFINITE) * Bounded(NEGATIVE_INFINITE, 0) + } + } + + @Test + fun testDivision() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM / Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) / BOTTOM) + assertEquals(BOTTOM, BOTTOM / Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) / BOTTOM) + assertEquals(BOTTOM, BOTTOM / BOTTOM) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(INFINITE, INFINITE) / Bounded(-5, 5) + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) / Bounded(-5, 5) + ) + assertEquals(Bounded(0, 0), Bounded(-5, 5) / Bounded(INFINITE, INFINITE)) + assertEquals(Bounded(0, 0), Bounded(-5, 5) / Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE)) + assertEquals(Bounded(5, 5), Bounded(-15, 15) / Bounded(-3, 3)) + + // Illegal Operations + assertThrows { + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) / Bounded(1, INFINITE) + } + assertThrows { + Bounded(INFINITE, INFINITE) / Bounded(NEGATIVE_INFINITE, 1) + } + assertThrows { + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) / Bounded(NEGATIVE_INFINITE, 1) + } + assertThrows { + Bounded(INFINITE, INFINITE) / Bounded(1, INFINITE) + } + assertThrows { Bounded(2, 4) / Bounded(1, 0) } + } + + @Test + fun testModulo() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM % Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) % BOTTOM) + assertEquals(BOTTOM, BOTTOM % Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) % BOTTOM) + assertEquals(BOTTOM, BOTTOM % BOTTOM) + + // Without BOTTOM + assertEquals(Bounded(0, 10), Bounded(INFINITE, INFINITE) % Bounded(5, 10)) + assertEquals(Bounded(0, 10), Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) % Bounded(5, 10)) + assertEquals(Bounded(-5, 5), Bounded(-5, 5) % Bounded(INFINITE, INFINITE)) + assertEquals(Bounded(-1, 1), Bounded(-10, 10) % Bounded(-3, 3)) + + // Illegal Operations + assertThrows { Bounded(-5, 5) % Bounded(0, 5) } + assertThrows { Bounded(-5, 5) % Bounded(-5, 0) } + assertThrows { + Bounded(-5, 5) % Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + } + } + + @Test + fun testJoin() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM.join(Bounded(5, 5))) + assertEquals(BOTTOM, Bounded(5, 5).join(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.join(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE).join(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.join(BOTTOM)) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, 10), + Bounded(5, 10).join(Bounded(NEGATIVE_INFINITE, -5)) + ) + assertEquals(Bounded(-10, INFINITE), Bounded(-10, -5).join(Bounded(5, INFINITE))) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(0, 0).join(Bounded(NEGATIVE_INFINITE, INFINITE)) + ) + assertEquals(Bounded(-10, 10), Bounded(9, 10).join(Bounded(-10, -9))) + } + + @Test + fun testMeet() { + // With BOTTOM + assertEquals(Bounded(5, 5), BOTTOM.meet(Bounded(5, 5))) + assertEquals(Bounded(5, 5), Bounded(5, 5).meet(BOTTOM)) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + BOTTOM.meet(Bounded(NEGATIVE_INFINITE, INFINITE)) + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, INFINITE).meet(BOTTOM) + ) + assertEquals(BOTTOM, BOTTOM.meet(BOTTOM)) + + // Without BOTTOM + assertEquals(BOTTOM, Bounded(5, 10).meet(Bounded(NEGATIVE_INFINITE, -5))) + assertEquals(BOTTOM, Bounded(-10, -5).meet(Bounded(5, INFINITE))) + assertEquals(BOTTOM, Bounded(9, 10).meet(Bounded(-10, -9))) + assertEquals(Bounded(0, 0), Bounded(0, 0).meet(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(Bounded(-9, 9), Bounded(-9, 10).meet(Bounded(-10, 9))) + } + + @Test + fun testWiden() { + // With BOTTOM + assertEquals(Bounded(5, 5), BOTTOM.widen(Bounded(5, 5))) + assertEquals(Bounded(5, 5), Bounded(5, 5).widen(BOTTOM)) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + BOTTOM.widen(Bounded(NEGATIVE_INFINITE, INFINITE)) + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, INFINITE).widen(BOTTOM) + ) + assertEquals(BOTTOM, BOTTOM.widen(BOTTOM)) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, 10), + Bounded(5, 10).widen(Bounded(NEGATIVE_INFINITE, -5)) + ) + assertEquals(Bounded(-10, INFINITE), Bounded(-10, -5).widen(Bounded(5, INFINITE))) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(0, 0).widen(Bounded(NEGATIVE_INFINITE, INFINITE)) + ) + assertEquals(Bounded(NEGATIVE_INFINITE, 10), Bounded(9, 10).widen(Bounded(-10, -9))) + assertEquals(Bounded(-10, INFINITE), Bounded(-10, -9).widen(Bounded(9, 10))) + } + + @Test + fun testNarrow() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM.narrow(Bounded(5, 5))) + assertEquals(BOTTOM, Bounded(5, 5).narrow(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.narrow(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE).narrow(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.narrow(BOTTOM)) + + // Without BOTTOM + assertEquals(Bounded(5, 10), Bounded(5, 10).narrow(Bounded(NEGATIVE_INFINITE, -5))) + assertEquals(Bounded(-10, -5), Bounded(-10, -5).narrow(Bounded(5, INFINITE))) + assertEquals(Bounded(0, 0), Bounded(0, 0).narrow(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(Bounded(-5, 5), Bounded(NEGATIVE_INFINITE, -5).narrow(Bounded(5, 10))) + assertEquals(Bounded(-5, 5), Bounded(5, INFINITE).narrow(Bounded(-10, -5))) + assertEquals(Bounded(0, 0), Bounded(NEGATIVE_INFINITE, INFINITE).narrow(Bounded(0, 0))) + } + + @Test + fun testToString() { + // assertEquals("BOTTOM", BOTTOM.toString()) + // assertEquals( + // "[NEGATIVE_INFINITE, INFINITE]", + // Bounded(NEGATIVE_INFINITE, INFINITE).toString() + // ) + assertEquals("[-5, 5]", Bounded(-5, 5).toString()) + } + + @Test + fun testWrapper() { + val bottomWrapper = IntervalLattice(BOTTOM) + val zeroWrapper = IntervalLattice(Bounded(0, 0)) + val outerWrapper = IntervalLattice(Bounded(-5, 5)) + val infinityWrapper = IntervalLattice(Bounded(NEGATIVE_INFINITE, INFINITE)) + + // compare to + assertEquals(-1, bottomWrapper.compareTo(zeroWrapper)) + assertEquals(0, zeroWrapper.compareTo(zeroWrapper)) + assertEquals(1, zeroWrapper.compareTo(bottomWrapper)) + + // contains + assertFalse(bottomWrapper.contains(zeroWrapper)) + assertFalse(zeroWrapper.contains(bottomWrapper)) + assertFalse(zeroWrapper.contains(outerWrapper)) + assertTrue(outerWrapper.contains(zeroWrapper)) + + // widen + assertEquals(outerWrapper, outerWrapper.widen(zeroWrapper)) + assertEquals(infinityWrapper, zeroWrapper.widen(outerWrapper)) + + // narrow + assertEquals(outerWrapper, infinityWrapper.narrow(outerWrapper)) + assertEquals(outerWrapper, outerWrapper.narrow(zeroWrapper)) + assertEquals(outerWrapper, outerWrapper.narrow(infinityWrapper)) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValueTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValueTest.kt new file mode 100644 index 0000000000..99b9a34740 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValueTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewArrayExpression +import kotlin.test.Test +import kotlin.test.assertEquals + +class ArrayValueTest { + private val name = Name("testVariable") + private val current = LatticeInterval.Bounded(1, 1) + + @Test + fun applyDeclarationTest() { + val correctDeclaration = run { + val decl = VariableDeclaration() + val init = NewArrayExpression() + val lit = Literal() + lit.value = 5 + init.dimensions = mutableListOf(lit) + decl.name = name + decl.initializer = init + decl + } + assertEquals( + LatticeInterval.Bounded(5, 5), + ArrayValue().applyEffect(current, correctDeclaration, name.localName) + ) + + val wrongNameDeclaration = run { + val decl = VariableDeclaration() + val init = Literal() + init.value = 5 + decl.name = Name("otherVariable") + decl.initializer = init + decl + } + assertEquals( + LatticeInterval.Bounded(1, 1), + ArrayValue().applyEffect(current, wrongNameDeclaration, name.localName) + ) + + val noInitializerDeclaration = run { + val decl = VariableDeclaration() + decl.name = name + decl + } + assertEquals( + LatticeInterval.Bounded(1, 1), + ArrayValue().applyEffect(current, noInitializerDeclaration, name.localName) + ) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValueTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValueTest.kt new file mode 100644 index 0000000000..1e61596486 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValueTest.kt @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bound.* +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator +import kotlin.test.Test +import kotlin.test.assertEquals + +class IntegerValueTest { + private val name = Name("testVariable") + private val current = LatticeInterval.Bounded(1, 1) + + @Test + fun applyDeclarationTest() { + val correctDeclaration = run { + val decl = VariableDeclaration() + val init = Literal() + init.value = 5 + decl.name = name + decl.initializer = init + decl + } + assertEquals( + LatticeInterval.Bounded(5, 5), + IntegerValue().applyEffect(current, correctDeclaration, name.localName) + ) + + val wrongNameDeclaration = run { + val decl = VariableDeclaration() + val init = Literal() + init.value = 5 + decl.name = Name("otherVariable") + decl.initializer = init + decl + } + assertEquals( + LatticeInterval.Bounded(1, 1), + IntegerValue().applyEffect(current, wrongNameDeclaration, name.localName) + ) + + val noInitializerDeclaration = run { + val decl = VariableDeclaration() + decl.name = name + decl + } + assertEquals( + LatticeInterval.Bounded(1, 1), + IntegerValue().applyEffect(current, noInitializerDeclaration, name.localName) + ) + } + + @Test + fun applyUnaryOperator() { + val preInc = run { + val op = UnaryOperator() + op.isPrefix = true + op.operatorCode = "++" + op.input.code = name.localName + op + } + assertEquals( + LatticeInterval.Bounded(2, 2), + IntegerValue().applyEffect(current, preInc, name.localName) + ) + + val postInc = run { + val op = UnaryOperator() + op.isPrefix = false + op.operatorCode = "++" + op.input.code = name.localName + op + } + assertEquals( + LatticeInterval.Bounded(2, 2), + IntegerValue().applyEffect(current, postInc, name.localName) + ) + + val preDec = run { + val op = UnaryOperator() + op.isPrefix = true + op.operatorCode = "--" + op.input.code = name.localName + op + } + assertEquals( + LatticeInterval.Bounded(0, 0), + IntegerValue().applyEffect(current, preDec, name.localName) + ) + + val postDec = run { + val op = UnaryOperator() + op.isPrefix = false + op.operatorCode = "--" + op.input.code = name.localName + op + } + assertEquals( + LatticeInterval.Bounded(0, 0), + IntegerValue().applyEffect(current, postDec, name.localName) + ) + + val wrongName = run { + val op = UnaryOperator() + op.isPrefix = false + op.operatorCode = "--" + op.input.code = "otherVariable" + op + } + assertEquals( + LatticeInterval.Bounded(1, 1), + IntegerValue().applyEffect(current, wrongName, name.localName) + ) + + val wrongCode = run { + val op = UnaryOperator() + op.isPrefix = false + op.operatorCode = "+-" + op.input.code = name.localName + op + } + assertEquals( + LatticeInterval.Bounded(1, 1), + IntegerValue().applyEffect(current, wrongCode, name.localName) + ) + } + + @Test + fun applyAssignExpression() { + val assignLiteral = run { + val expr = AssignExpression() + val ref = Reference() + val lit = Literal() + lit.value = 3 + ref.code = name.localName + expr.operatorCode = "=" + expr.lhs.add(0, ref) + expr.rhs.add(0, lit) + expr + } + assertEquals( + LatticeInterval.Bounded(3, 3), + IntegerValue().applyEffect(current, assignLiteral, name.localName) + ) + + val assignFallback = run { + val expr = AssignExpression() + val ref = Reference() + ref.code = name.localName + expr.operatorCode = "=" + expr.lhs.add(0, ref) + expr + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(current, assignFallback, name.localName) + ) + + val assignPlusLiteral = run { + val expr = AssignExpression() + val ref = Reference() + val lit = Literal() + lit.value = 3 + ref.code = name.localName + expr.operatorCode = "+=" + expr.lhs.add(0, ref) + expr.rhs.add(0, lit) + expr + } + assertEquals( + LatticeInterval.Bounded(4, 4), + IntegerValue().applyEffect(current, assignPlusLiteral, name.localName) + ) + + val assignPlusFallback = run { + val expr = AssignExpression() + val ref = Reference() + ref.code = name.localName + expr.operatorCode = "+=" + expr.lhs.add(0, ref) + expr + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(current, assignPlusFallback, name.localName) + ) + + val assignMinusLiteral = run { + val expr = AssignExpression() + val ref = Reference() + val lit = Literal() + lit.value = 3 + ref.code = name.localName + expr.operatorCode = "-=" + expr.lhs.add(0, ref) + expr.rhs.add(0, lit) + expr + } + assertEquals( + LatticeInterval.Bounded(-2, -2), + IntegerValue().applyEffect(current, assignMinusLiteral, name.localName) + ) + + val assignMinusFallback = run { + val expr = AssignExpression() + val ref = Reference() + ref.code = name.localName + expr.operatorCode = "-=" + expr.lhs.add(0, ref) + expr + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(current, assignMinusFallback, name.localName) + ) + + val assignTimesLiteral = run { + val expr = AssignExpression() + val ref = Reference() + val lit = Literal() + lit.value = 3 + ref.code = name.localName + expr.operatorCode = "*=" + expr.lhs.add(0, ref) + expr.rhs.add(0, lit) + expr + } + assertEquals( + LatticeInterval.Bounded(3, 3), + IntegerValue().applyEffect(current, assignTimesLiteral, name.localName) + ) + + val assignTimesFallback = run { + val expr = AssignExpression() + val ref = Reference() + ref.code = name.localName + expr.operatorCode = "*=" + expr.lhs.add(0, ref) + expr + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(current, assignTimesFallback, name.localName) + ) + + val assignDivLiteral = run { + val expr = AssignExpression() + val ref = Reference() + val lit = Literal() + lit.value = 3 + ref.code = name.localName + expr.operatorCode = "/=" + expr.lhs.add(0, ref) + expr.rhs.add(0, lit) + expr + } + assertEquals( + LatticeInterval.Bounded(0, 0), + IntegerValue().applyEffect(current, assignDivLiteral, name.localName) + ) + + val assignDivFallback = run { + val expr = AssignExpression() + val ref = Reference() + ref.code = name.localName + expr.operatorCode = "/=" + expr.lhs.add(0, ref) + expr + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(current, assignDivFallback, name.localName) + ) + + val assignModLiteral = run { + val expr = AssignExpression() + val ref = Reference() + val lit = Literal() + lit.value = 3 + ref.code = name.localName + expr.operatorCode = "%=" + expr.lhs.add(0, ref) + expr.rhs.add(0, lit) + expr + } + assertEquals( + LatticeInterval.Bounded(1, 1), + IntegerValue().applyEffect(current, assignModLiteral, name.localName) + ) + + val assignModFallback = run { + val expr = AssignExpression() + val ref = Reference() + ref.code = name.localName + expr.operatorCode = "%=" + expr.lhs.add(0, ref) + expr + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(current, assignModFallback, name.localName) + ) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValueTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValueTest.kt new file mode 100644 index 0000000000..8b2179a707 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValueTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bound.* +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import kotlin.test.Test +import kotlin.test.assertEquals + +class MutableListValueTest { + private val name = Name("testVariable") + private val current = LatticeInterval.Bounded(1, 1) + + @Test + fun applyDeclarationTest() { + val correctDeclaration = run { + val decl = VariableDeclaration() + val init = MemberCallExpression() + val lit = Literal() + lit.value = 5 + init.arguments = mutableListOf(lit, lit) + decl.name = name + decl.initializer = init + decl + } + assertEquals( + LatticeInterval.Bounded(2, 2), + MutableListValue().applyEffect(current, correctDeclaration, name.localName) + ) + + val wrongNameDeclaration = run { + val decl = VariableDeclaration() + val init = Literal() + init.value = 5 + decl.name = Name("otherVariable") + decl.initializer = init + decl + } + assertEquals( + LatticeInterval.Bounded(1, 1), + MutableListValue().applyEffect(current, wrongNameDeclaration, name.localName) + ) + + val noInitializerDeclaration = run { + val decl = VariableDeclaration() + decl.name = name + decl + } + assertEquals( + LatticeInterval.Bounded(1, 1), + MutableListValue().applyEffect(current, noInitializerDeclaration, name.localName) + ) + } + + @Test + fun applyDirectCallTest() { + val add = run { + val expr = MemberCallExpression() + val member = MemberExpression() + member.base.code = name.localName + member.name = Name("add") + expr.callee = member + expr + } + assertEquals( + LatticeInterval.Bounded(2, 2), + MutableListValue().applyEffect(current, add, name.localName) + ) + + val addAll = run { + val expr = MemberCallExpression() + val member = MemberExpression() + member.base.code = name.localName + member.name = Name("addAll") + expr.callee = member + expr + } + assertEquals( + LatticeInterval.Bounded(1, INFINITE), + MutableListValue().applyEffect(current, addAll, name.localName) + ) + + val clear = run { + val expr = MemberCallExpression() + val member = MemberExpression() + member.base.code = name.localName + member.name = Name("clear") + expr.callee = member + expr + } + assertEquals( + LatticeInterval.Bounded(0, 0), + MutableListValue().applyEffect(current, clear, name.localName) + ) + + val removeInt = run { + val expr = MemberCallExpression() + val lit = Literal() + val member = MemberExpression() + member.base.code = name.localName + member.name = Name("remove") + lit.type = IntegerType() + expr.callee = member + expr.arguments = mutableListOf(lit) + expr + } + assertEquals( + LatticeInterval.Bounded(0, 0), + MutableListValue().applyEffect(current, removeInt, name.localName) + ) + + val removeObject = run { + val expr = MemberCallExpression() + val lit = Literal() + val member = MemberExpression() + member.base.code = name.localName + member.name = Name("remove") + expr.callee = member + expr.arguments = mutableListOf(lit) + expr + } + assertEquals( + LatticeInterval.Bounded(0, 1), + MutableListValue().applyEffect(current, removeObject, name.localName) + ) + + val removeAll = run { + val expr = MemberCallExpression() + val member = MemberExpression() + member.base.code = name.localName + member.name = Name("removeAll") + expr.callee = member + expr + } + assertEquals( + LatticeInterval.Bounded(0, 1), + MutableListValue().applyEffect(current, removeAll, name.localName) + ) + + val wrongName = run { + val expr = MemberCallExpression() + val member = MemberExpression() + member.name = Name("add") + expr.callee = member + expr + } + assertEquals( + LatticeInterval.Bounded(1, 1), + MutableListValue().applyEffect(current, wrongName, name.localName) + ) + } + + @Test fun applyIndirectCallTest() {} +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/AbstractEvaluationTests.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/AbstractEvaluationTests.kt new file mode 100644 index 0000000000..bab3beff32 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/AbstractEvaluationTests.kt @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.testcases + +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.frontends.TestLanguage +import de.fraunhofer.aisec.cpg.graph.builder.* +import de.fraunhofer.aisec.cpg.passes.UnreachableEOGPass + +abstract class AbstractEvaluationTests { + companion object { + /* + public class IntegerTest { + public void f1() { + Bar b = new Bar(); + int a = 5; + + a = 0; + a -= 2; + a += 3; + + b.f(a); + } + + public void f2() { + Bar f = new Bar(); + int a = 5; + + a = 3; + a++; + ++a; + a -= 2; + a += 3; + a--; + --a; + a *= 4; + a /= 2; + a %= 3; + + b.f(a); + } + + public void f3() { + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } + + b.f(a); + } + + public void f4() { + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } else { + a = 3; + } + + b.f(a); + } + + public void f5() { + Bar b = new Bar(); + int a = 5; + + for (int i = 0; i < 5; i++) { + a += 1; + } + + b.f(a); + } + } + + class Bar { + public void f(int a) {} + } + */ + fun getIntegerExample( + config: TranslationConfiguration = + TranslationConfiguration.builder() + .defaultPasses() + .registerLanguage(TestLanguage(".")) + .registerPass() + .build() + ) = + testFrontend(config).build { + translationResult { + translationUnit("integer.java") { + record("Foo") { + method("f1") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ref("a") assign literal(0, t("int")) + ref("a") assignMinus literal(2, t("int")) + ref("a") assignPlus literal(3, t("int")) + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f2") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ref("a") assign literal(3, t("int")) + + ref("a").inc() + ref("a").incPrefix() + + ref("a") assignMinus literal(2, t("int")) + ref("a") assignPlus literal(3, t("int")) + + ref("a").dec() + ref("a").decPrefix() + + ref("a") assignMult literal(4, t("int")) + ref("a") assignDiv literal(2, t("int")) + ref("a") assignMod literal(3, t("int")) + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f3") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ifStmt { + condition { memberCall("nextBoolean", ref("Random")) } + thenStmt { ref("a") assignMinus literal(1, t("int")) } + } + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f4") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ifStmt { + condition { memberCall("nextBoolean", ref("Random")) } + thenStmt { ref("a") assignMinus literal(1, t("int")) } + elseStmt { ref("a") assign literal(3, t("int")) } + } + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f5") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + forStmt( + declare { + variable("i", t("int")) { literal(0, t("int")) } + }, + ref("i") lt literal(5, t("int")), + ref("i").inc() + ) { + ref("a") assignPlus literal(1, t("int")) + } + + memberCall("f", ref("Bar")) { ref("a") } + } + } + } + record("Bar") { + method("main") { + param("a", t("int")) + body {} + } + } + } + } + } + } +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt index 1ea29ec589..57335ee7f4 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt @@ -968,6 +968,7 @@ fun LanguageFrontend<*, *>.ref( ): Reference { val node = newReference(name) node.type = type + node.code = name.toString() if (init != null) { init(node) @@ -1182,8 +1183,8 @@ fun reference(input: Expression): UnaryOperator { } /** - * Creates a new [UnaryOperator] with a `--` [UnaryOperator.operatorCode] in the Fluent Node DSL and - * adds it to the nearest enclosing [StatementHolder]. + * Creates a new [UnaryOperator] with a `--` postfix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. */ context(LanguageFrontend<*, *>, Holder) operator fun Expression.dec(): UnaryOperator { @@ -1198,8 +1199,8 @@ operator fun Expression.dec(): UnaryOperator { } /** - * Creates a new [UnaryOperator] with a `++` [UnaryOperator.operatorCode] in the Fluent Node DSL and - * invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder]. + * Creates a new [UnaryOperator] with a `++` postfix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder]. */ context(LanguageFrontend<*, *>, Holder) operator fun Expression.inc(): UnaryOperator { @@ -1213,6 +1214,38 @@ operator fun Expression.inc(): UnaryOperator { return node } +/** + * Creates a new [UnaryOperator] with a `--` prefix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(LanguageFrontend<*, *>, Holder) +fun Expression.decPrefix(): UnaryOperator { + val node = (this@LanguageFrontend).newUnaryOperator("--", false, true) + node.input = this + + if (this@Holder is StatementHolder) { + this@Holder += node + } + + return node +} + +/** + * Creates a new [UnaryOperator] with a `++` prefix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder]. + */ +context(LanguageFrontend<*, *>, Holder) +fun Expression.incPrefix(): UnaryOperator { + val node = (this@LanguageFrontend).newUnaryOperator("++", false, true) + node.input = this + + if (this@Holder is StatementHolder) { + this@Holder += node + } + + return node +} + /** * Creates a new [UnaryOperator] with a `++` [UnaryOperator.operatorCode] in the Fluent Node DSL and * invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder]. @@ -1388,6 +1421,66 @@ infix fun Expression.assignPlus(rhs: Expression): AssignExpression { return node } +/** + * Creates a new [AssignExpression] with a `-=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(LanguageFrontend<*, *>, Holder) +infix fun Expression.assignMinus(rhs: Expression): AssignExpression { + val node = (this@LanguageFrontend).newAssignExpression("-=", listOf(this), listOf(rhs)) + + if (this@Holder is StatementHolder) { + this@Holder += node + } + + return node +} + +/** + * Creates a new [AssignExpression] with a `*=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(LanguageFrontend<*, *>, Holder) +infix fun Expression.assignMult(rhs: Expression): AssignExpression { + val node = (this@LanguageFrontend).newAssignExpression("*=", listOf(this), listOf(rhs)) + + if (this@Holder is StatementHolder) { + this@Holder += node + } + + return node +} + +/** + * Creates a new [AssignExpression] with a `/=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(LanguageFrontend<*, *>, Holder) +infix fun Expression.assignDiv(rhs: Expression): AssignExpression { + val node = (this@LanguageFrontend).newAssignExpression("/=", listOf(this), listOf(rhs)) + + if (this@Holder is StatementHolder) { + this@Holder += node + } + + return node +} + +/** + * Creates a new [AssignExpression] with a `%=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(LanguageFrontend<*, *>, Holder) +infix fun Expression.assignMod(rhs: Expression): AssignExpression { + val node = (this@LanguageFrontend).newAssignExpression("%=", listOf(this), listOf(rhs)) + + if (this@Holder is StatementHolder) { + this@Holder += node + } + + return node +} + /** * Creates a new [AssignExpression] with a `=` [AssignExpression.operatorCode] in the Fluent Node * DSL and adds it to the nearest enclosing [StatementHolder]. diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt index f9e4f3c864..c04525a8ff 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt @@ -73,9 +73,10 @@ class PowersetLattice(override val elements: IdentitySet) : } /** - * Stores the current state. I.e., it maps [K] (e.g. a [Node] or [Edge]) to a [LatticeElement]. It - * provides some useful functions e.g. to check if the mapping has to be updated (e.g. because there - * are new nodes or because a new lattice element is bigger than the old one). + * Stores the current state. I.e., it maps [K] (e.g. a [Node] or [PropertyEdge]) to a + * [LatticeElement]. It provides some useful functions e.g. to check if the mapping has to be + * updated (e.g. because there are new nodes or because a new lattice element is bigger than the old + * one). */ open class State : HashMap>() { @@ -148,12 +149,19 @@ open class State : HashMap>() { */ class Worklist() { /** A mapping of nodes to the state which is currently available there. */ - var globalState = IdentityHashMap>() - private set + private var globalState = IdentityHashMap>() /** A list of all nodes which have already been visited. */ private val alreadySeen = IdentitySet() + enum class EvaluationState { + WIDENING, + NARROWING, + DONE + } + + val evaluationStateMap = IdentityHashMap() + constructor( globalState: IdentityHashMap> = IdentityHashMap>() ) : this() { @@ -218,6 +226,16 @@ class Worklist() { /** Checks if [currentNode] has already been visited before. */ fun hasAlreadySeen(currentNode: K) = currentNode in alreadySeen + /** Checks if [currentNode] needs to be widened. */ + fun needsWidening(currentNode: K) = evaluationStateMap[currentNode] == EvaluationState.WIDENING + + /** Checks if [currentNode] needs to be narrowed. */ + fun needsNarrowing(currentNode: K) = + evaluationStateMap[currentNode] == EvaluationState.NARROWING + + /** Checks if [currentNode] should not be changed anymore. */ + fun isDone(currentNode: K) = evaluationStateMap[currentNode] == EvaluationState.DONE + /** Computes the meet over paths for all the states in [globalState]. */ fun mop(): State? { val firstKey = globalState.keys.firstOrNull() @@ -247,48 +265,12 @@ inline fun iterateEOG( return iterateEOG(startNode, startState) { k, s, _ -> transformation(k, s) } } -/** - * Iterates through the worklist of the Evaluation Order Graph starting at [startNode] and with the - * [State] [startState]. For each node, the [transformation] is applied which should update the - * state. - * - * [transformation] receives the current [Node] popped from the worklist, the [State] at this node - * which is considered for this analysis and even the current [Worklist]. The worklist is given if - * we have to add more elements out-of-order e.g. because the EOG is traversed in an order which is - * not useful for this analysis. The [transformation] has to return the updated [State]. - */ inline fun iterateEOG( startNode: K, startState: State, transformation: (K, State, Worklist) -> State ): State? { - val initialState = IdentityHashMap>() - initialState[startNode] = startState - val worklist = Worklist(initialState) - worklist.push(startNode, startState) - - while (worklist.isNotEmpty()) { - val (nextNode, state) = worklist.pop() - - // This should check if we're not near the beginning/end of a basic block (i.e., there are - // no merge points or branches of the EOG nearby). If that's the case, we just parse the - // whole basic block and do not want to duplicate the state. Near the beginning/end, we do - // want to copy the state to avoid terminating the iteration too early by messing up with - // the state-changing checks. - val insideBB = - (nextNode.nextEOGEdges.size == 1 && - nextNode.prevEOGEdges.singleOrNull()?.start?.nextEOG?.size == 1) - val newState = - transformation(nextNode, if (insideBB) state else state.duplicate(), worklist) - if (worklist.update(nextNode, newState)) { - nextNode.nextEOGEdges.forEach { - if (it is K) { - worklist.push(it, newState) - } - } - } - } - return worklist.mop() + return iterateEOG(startNode, startState, transformation, null) } inline fun , N : Any, V> iterateEOG( @@ -335,3 +317,48 @@ inline fun , N : Any, V> iterateEOG( } return worklist.mop() } + +/** + * Iterates through the worklist of the Evaluation Order Graph starting at [startNode] and with the + * [State] [startState]. For each node, the [transformation] is applied which should update the + * state. When the [until] node is reached, its successors are not added to the worklist. + * + * [transformation] receives the current [Node] popped from the worklist, the [State] at this node + * which is considered for this analysis and even the current [Worklist]. The worklist is given if + * we have to add more elements out-of-order e.g. because the EOG is traversed in an order which is + * not useful for this analysis. The [transformation] has to return the updated [State]. + */ +inline fun iterateEOG( + startNode: K, + startState: State, + transformation: (K, State, Worklist) -> State, + until: Node? +): State? { + val initialState = IdentityHashMap>() + initialState[startNode] = startState + val worklist = Worklist(initialState) + worklist.push(startNode, startState) + + while (worklist.isNotEmpty()) { + val (nextNode, state) = worklist.pop() + + // This should check if we're not near the beginning/end of a basic block (i.e., there are + // no merge points or branches of the EOG nearby). If that's the case, we just parse the + // whole basic block and do not want to duplicate the state. Near the beginning/end, we do + // want to copy the state to avoid terminating the iteration too early by messing up with + // the state-changing checks. + val insideBB = + (nextNode.nextEOG.size == 1 && nextNode.prevEOG.singleOrNull()?.nextEOG?.size == 1) + val newState = + transformation(nextNode, if (insideBB) state else state.duplicate(), worklist) + + if (worklist.update(nextNode, newState) && nextNode != until) { + nextNode.nextEOG.forEach { + if (it is K && !worklist.isDone(it)) { + worklist.push(it, newState) + } + } + } + } + return worklist.mop() +}