From e1f3b9183551d6f23286a945914a4f21ea0d9ebf Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 12 Mar 2026 13:01:18 +0100 Subject: [PATCH 1/5] Create DualGraph.qll --- shared/util/codeql/util/DualGraph.qll | 92 +++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 shared/util/codeql/util/DualGraph.qll diff --git a/shared/util/codeql/util/DualGraph.qll b/shared/util/codeql/util/DualGraph.qll new file mode 100644 index 000000000000..faeb1e498be4 --- /dev/null +++ b/shared/util/codeql/util/DualGraph.qll @@ -0,0 +1,92 @@ +/** + * Provides an efficient mechanism for checking if two nodes have + * a common ancestor in a graph. + */ + +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) + + /** 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 = TForward(result) } + + /** Gets the underlying node if this is a backward node. */ + Node asBackward() { this = TBackward(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() + } + } + + /** Gets the node representing the backward node wrapping `n`. */ + DualNode getBackwardNode(Node n) { result.asBackward() = n } + + /** Gets the node representing the forward node wrapping `n`. */ + DualNode getForwardNode(Node n) { result.asForward() = n } + + /** + * Holds if the dual graph contains the edge `node1 -> node2`. See `MakeDualGraph`. + */ + 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) + + /** + * 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. + */ + 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)) + } +} From 83d9d580096ffec2815c0107d41a73edb7a3f364 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 12 Mar 2026 13:08:34 +0100 Subject: [PATCH 2/5] Ruby: use dual graph for common module ancestor check --- .../dataflow/internal/DataFlowPrivate.qll | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) 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) } /** From fb85c7ff37f3a2ea61328bd1da8c59c58b22620e Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 12 Mar 2026 13:12:03 +0100 Subject: [PATCH 3/5] Add missing overlay annotation --- shared/util/codeql/util/DualGraph.qll | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/util/codeql/util/DualGraph.qll b/shared/util/codeql/util/DualGraph.qll index faeb1e498be4..562e2c23bb68 100644 --- a/shared/util/codeql/util/DualGraph.qll +++ b/shared/util/codeql/util/DualGraph.qll @@ -2,6 +2,8 @@ * Provides an efficient mechanism for checking if two nodes have * a common ancestor in a graph. */ +overlay[local?] +module; private import Location From b26dcc088bbd4f2f5cbe24d76cfebb8ec68775fb Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 12 Mar 2026 13:17:02 +0100 Subject: [PATCH 4/5] Add overlay[caller?] --- shared/util/codeql/util/DualGraph.qll | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/util/codeql/util/DualGraph.qll b/shared/util/codeql/util/DualGraph.qll index 562e2c23bb68..7a107cdd1920 100644 --- a/shared/util/codeql/util/DualGraph.qll +++ b/shared/util/codeql/util/DualGraph.qll @@ -85,6 +85,7 @@ module MakeDualGraph Input> { * 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 From a109302c9f2765446d912bea25e6ce3d7754381e Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 12 Mar 2026 13:19:22 +0100 Subject: [PATCH 5/5] Stricter caching --- shared/util/codeql/util/DualGraph.qll | 57 +++++++++++++++------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/shared/util/codeql/util/DualGraph.qll b/shared/util/codeql/util/DualGraph.qll index 7a107cdd1920..0893017ae4c7 100644 --- a/shared/util/codeql/util/DualGraph.qll +++ b/shared/util/codeql/util/DualGraph.qll @@ -35,13 +35,43 @@ module MakeDualGraph Input> { 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 = TForward(result) } + Node asForward() { this = getForwardNode(result) } /** Gets the underlying node if this is a backward node. */ - Node asBackward() { this = TBackward(result) } + Node asBackward() { this = getBackwardNode(result) } /** Gets a string representation of this node. */ string toString() { @@ -58,29 +88,6 @@ module MakeDualGraph Input> { } } - /** Gets the node representing the backward node wrapping `n`. */ - DualNode getBackwardNode(Node n) { result.asBackward() = n } - - /** Gets the node representing the forward node wrapping `n`. */ - DualNode getForwardNode(Node n) { result.asForward() = n } - - /** - * Holds if the dual graph contains the edge `node1 -> node2`. See `MakeDualGraph`. - */ - 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) - /** * 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.