diff --git a/ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll b/ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll index 9332aa43e52b..13d3f99e58e4 100644 --- a/ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll +++ b/ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll @@ -2036,15 +2036,25 @@ private predicate compatibleTypesNonSymRefl(DataFlowType t1, DataFlowType t2) { isCollectionClass(t2) } -pragma[nomagic] -private predicate compatibleModuleTypes(TModuleDataFlowType t1, TModuleDataFlowType t2) { - exists(Module m1, Module m2, Module m3 | - t1 = TModuleDataFlowType(m1) and - t2 = TModuleDataFlowType(m2) - | - m3.getAnAncestor() = m1 and - m3.getAnAncestor() = m2 - ) +private import codeql.util.DualGraph + +private module ModuleDualGraphInput implements DualGraphInputSig { + class Node = DataFlowType; + + predicate edge(Node node1, Node node2) { + exists(Module m1, Module m2 | + node1 = TModuleDataFlowType(m1) and + node2 = TModuleDataFlowType(m2) and + m1.getAnImmediateAncestor() = m2 + ) + } +} + +module ModuleDualGraph = MakeDualGraph; + +pragma[inline] +private predicate compatibleModuleTypes(DataFlowType t1, DataFlowType t2) { + ModuleDualGraph::hasCommonAncestor(t1, t2) } /** diff --git a/shared/util/codeql/util/DualGraph.qll b/shared/util/codeql/util/DualGraph.qll new file mode 100644 index 000000000000..0893017ae4c7 --- /dev/null +++ b/shared/util/codeql/util/DualGraph.qll @@ -0,0 +1,102 @@ +/** + * Provides an efficient mechanism for checking if two nodes have + * a common ancestor in a graph. + */ +overlay[local?] +module; + +private import Location + +signature module DualGraphInputSig { + class Node { + string toString(); + + Location getLocation(); + } + + predicate edge(Node node1, Node node2); +} + +/** + * Creates a "dual graph" in which each node in the given graph has a "forward" and "backward" + * copy. + * + * All original edges are present in both copies, but reversed in the backward copy. + * + * In addition, all nodes have an edge from their backward node to their forward node. + * + * This can be used to check if two nodes have a common ancestor in the graph, by checking + * if a path exists from the reverse node of one node, to the forward node of another. + */ +module MakeDualGraph Input> { + private import Input + + private newtype TDualNode = + TForward(Node n) or + TBackward(Node n) + + cached + private module Cached { + /** Gets the node representing the backward node wrapping `n`. */ + cached + DualNode getBackwardNode(Node n) { result = TBackward(n) } + + /** Gets the node representing the forward node wrapping `n`. */ + cached + DualNode getForwardNode(Node n) { result = TForward(n) } + + /** + * Holds if the dual graph contains the edge `node1 -> node2`. See `MakeDualGraph`. + */ + private predicate dualEdge(DualNode node1, DualNode node2) { + edge(node1.asForward(), node2.asForward()) + or + edge(node2.asBackward(), node1.asBackward()) + or + node1.asBackward() = node2.asForward() + } + + /** + * Holds if there is a non-empty path from `node1 -> node2` in the dual graph. + */ + cached + predicate dualPath(DualNode node1, DualNode node2) = fastTC(dualEdge/2)(node1, node2) + } + + import Cached + + /** A forward or backward copy of a node from the original graph. */ + class DualNode extends TDualNode { + /** Gets the underlying node if this is a forward node. */ + Node asForward() { this = getForwardNode(result) } + + /** Gets the underlying node if this is a backward node. */ + Node asBackward() { this = getBackwardNode(result) } + + /** Gets a string representation of this node. */ + string toString() { + result = this.asForward().toString() + or + result = "[rev] " + this.asBackward().toString() + } + + /** Gets the location of this node. */ + Location getLocation() { + result = this.asForward().getLocation() + or + result = this.asBackward().getLocation() + } + } + + /** + * Holds if `node1` and `node2` have a common ancestor in the original graph, that is, + * there exists a node from which both nodes are reachable. + */ + overlay[caller?] + pragma[inline] + predicate hasCommonAncestor(Node node1, Node node2) { + // Note: `fastTC` only checks for non-empty paths, but there is no need to special-case + // `node1 = node2` because the path `Backward(n) -> Forward(n)` is non-empty. + dualPath(getBackwardNode(node1), getForwardNode(node2)) + } +}