Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7f608c3
Python: add new shared-SSA-backed SSA adapter
Copilot Jun 1, 2026
b73d583
Python: remove unnecessary change note
yoff Jul 28, 2026
0ec5257
Python: remove superflous module
yoff Jul 28, 2026
6e5dcfa
Python: attempt at instantiating MakeSsa
yoff Jul 28, 2026
41c95a2
Python: fix compilation of test
yoff Jul 30, 2026
4e80764
Potential fix for pull request finding
yoff Jul 30, 2026
ad2446f
Potential fix for pull request finding
yoff Jul 30, 2026
d5e51c5
Python: address bot reviews
yoff Jul 30, 2026
7261f28
Python: exclude constants from being SSA variables
yoff Aug 6, 2026
6692154
Python: test phi definitions at module exit
yoff Aug 19, 2026
a609200
Python: preserve phi uses at module exit
yoff Aug 19, 2026
afb6a99
Python: switch dataflow library to new (shared) CFG + SSA
yoff Jun 4, 2026
99da2dd
Python: preserve bindingset on new-CFG node dominance wrappers
yoff Jul 2, 2026
62b45bb
Python: fix shared CFG facade CI checks
yoff Aug 20, 2026
00f24b8
Python: test direct guard specialization semantics
yoff Aug 28, 2026
7bf61bc
Python: specialize direct truthiness guards
yoff Aug 28, 2026
df7127a
Python: unpin legacy CFG/ESSA from the AST cached stage
yoff Jul 2, 2026
e271e3c
Add captured call target diagnostic
yoff Aug 21, 2026
393408e
Bind captured type-tracking jumps from their source
yoff Aug 21, 2026
391330e
Python: test exposed shared SSA adjacency relations
yoff Aug 25, 2026
7bef6d2
Python: cache exposed shared SSA adjacency relations
yoff Aug 25, 2026
f58c3ca
SSA: add opt-in cached liveness construction
yoff Aug 30, 2026
f711417
Python: cache shared SSA liveness across stages
yoff Aug 30, 2026
8dc8995
SSA: add opt-in definition reachability caching
yoff Aug 31, 2026
c9a90f9
Python: cache shared SSA definition reachability
yoff Aug 31, 2026
8dff23d
Python: cache shared CFG scope
yoff Aug 31, 2026
342ed91
Cache Python shared CFG node mapping
yoff Sep 1, 2026
8a34a9e
Bind explicit CFG successors before expansion
yoff Sep 2, 2026
d75f96a
Python: rebless implicit import SSA definitions
yoff Sep 3, 2026
80939b0
Python: prefilter directly controlled SSA uses
yoff Sep 3, 2026
17e9303
Remove broad SSA caches for Python facade
yoff Sep 8, 2026
8be73c6
Add MakeSsa facade contract
yoff Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions python/ql/consistency-queries/DataFlowConsistency.ql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.internal.DataFlowImplSpecific
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import semmle.python.dataflow.new.internal.TaintTrackingImplSpecific
private import codeql.dataflow.internal.DataFlowImplConsistency
private import semmle.python.controlflow.internal.Cfg as Cfg

private module Input implements InputSig<Location, PythonDataFlow> {
private import Private
Expand Down Expand Up @@ -74,7 +75,7 @@ private module Input implements InputSig<Location, PythonDataFlow> {
// resolve to multiple functions), but we only make _one_ ArgumentNode for each
// argument in the CallNode, we end up violating this consistency check in those
// cases. (see `getCallArg` in DataFlowDispatch.qll)
exists(DataFlowCall other, CallNode cfgCall | other != call |
exists(DataFlowCall other, Cfg::CallNode cfgCall | other != call |
call.getNode() = cfgCall and
other.getNode() = cfgCall and
isArgumentNode(arg, call, _) and
Expand All @@ -90,16 +91,16 @@ private module Input implements InputSig<Location, PythonDataFlow> {
// allow it instead.
(
call.getScope() = attr.getScope() and
any(CfgNode n | n.asCfgNode() = call.getNode().(CallNode).getFunction()).getALocalSource() =
attr
any(CfgNode n | n.asCfgNode() = call.getNode().(Cfg::CallNode).getFunction())
.getALocalSource() = attr
or
not exists(call.getScope().(Function).getDefinition()) and
call.getScope().getScope+() = attr.getScope()
) and
(
other.getScope() = attr.getScope() and
any(CfgNode n | n.asCfgNode() = other.getNode().(CallNode).getFunction()).getALocalSource() =
attr
any(CfgNode n | n.asCfgNode() = other.getNode().(Cfg::CallNode).getFunction())
.getALocalSource() = attr
or
not exists(other.getScope().(Function).getDefinition()) and
other.getScope().getScope+() = attr.getScope()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: breaking
---
* The deprecated `AstNode.getAFlowNode()` and `Function.getAReturnValueFlowNode()` predicates now return nodes from the new shared CFG (`Cfg::ControlFlowNode`) rather than from the legacy CFG (`ControlFlowNode`). Callers that still rely on these deprecated APIs and feed the result into legacy-CFG-aware predicates will no longer type-check; migrate to `n.getNode() = e` (or, for return values, the explicit `Return` pattern shown in the deprecation message) to get nodes from the dataflow library's current CFG.
31 changes: 18 additions & 13 deletions python/ql/lib/semmle/python/ApiGraphs.qll
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* directed and labeled; they specify how the components represented by nodes relate to each other.
*/

// Importing python under the `py` namespace to avoid importing `CallNode` from `Flow.qll` and thereby having a naming conflict with `API::CallNode`.
// Importing python under the `PY` namespace to avoid pulling in `CallNode` from `Flow.qll` (via `import python`) and thereby having a naming conflict with `API::CallNode`.
private import python as PY
private import semmle.python.controlflow.internal.Cfg as Cfg
import semmle.python.dataflow.new.DataFlow
private import semmle.python.internal.CachedStages

Expand Down Expand Up @@ -282,15 +283,19 @@ module API {
index = this.getIndex() and
(
// subscripting
exists(PY::SubscriptNode subscript |
subscript.getObject() = this.getAValueReachableFromSource().asCfgNode() and
subscript.getIndex() = index.asSink().asCfgNode()
|
// reading
subscript = result.asSource().asCfgNode()
or
// writing
subscript.(PY::DefinitionNode).getValue() = result.asSink().asCfgNode()
exists(Cfg::SubscriptNode subscript |
// Bind `subscript` from the already-pinned `result` edge first, so the
// (expensive, large) `getAValueReachableFromSource` join below is a
// membership check on a bound object rather than a cross-product.
(
// reading
subscript = result.asSource().asCfgNode()
or
// writing
subscript.(Cfg::DefinitionNode).getValue() = result.asSink().asCfgNode()
) and
subscript.getIndex() = index.asSink().asCfgNode() and
subscript.getObject() = this.getAValueReachableFromSource().asCfgNode()
)
or
// dictionary literals
Expand Down Expand Up @@ -684,7 +689,7 @@ module API {
* Ignores relative imports, such as `from ..foo.bar import baz`.
*/
private predicate imports(DataFlow::CfgNode imp, string name) {
exists(PY::ImportExprNode iexpr |
exists(Cfg::ImportExprNode iexpr |
imp.getNode() = iexpr and
not iexpr.getNode().isRelative() and
name = iexpr.getNode().getImportedModuleName()
Expand Down Expand Up @@ -775,7 +780,7 @@ module API {
// list literals, from `x` to `[x]`
// TODO: once convenient, this should be done at a higher level than the AST,
// at least at the CFG layer, to take splitting into account.
// Also consider `SequenceNode for generality.
// Also consider `Cfg::SequenceNode` for generality.
exists(PY::List list | list = pred.(DataFlow::ExprNode).getNode().getNode() |
rhs.(DataFlow::ExprNode).getNode().getNode() = list.getAnElt() and
lbl = Label::subscript()
Expand Down Expand Up @@ -805,7 +810,7 @@ module API {
subscript = trackUseNode(src).getSubscript(index)
|
// from `x` to a definition of `x[...]`
rhs.asCfgNode() = subscript.asCfgNode().(PY::DefinitionNode).getValue() and
rhs.asCfgNode() = subscript.asCfgNode().(Cfg::DefinitionNode).getValue() and
lbl = Label::subscript()
or
// from `x` to `"key"` in `x["key"]`
Expand Down
14 changes: 7 additions & 7 deletions python/ql/lib/semmle/python/AstExtended.qll
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module;

import python
private import semmle.python.internal.CachedStages
private import semmle.python.controlflow.internal.Cfg as Cfg

/** A syntactic node (Class, Function, Module, Expr, Stmt or Comprehension) corresponding to a flow node */
abstract class AstNode extends AstNode_ {
Expand All @@ -19,17 +20,16 @@ abstract class AstNode extends AstNode_ {
/**
* DEPRECATED: use `ControlFlowNode.getNode()` from the other direction instead;
* that is, replace `e.getAFlowNode() = n` with `n.getNode() = e`. This API is
* being removed to untangle the AST and CFG hierarchies in preparation for
* migrating the dataflow library off the legacy CFG.
* being removed to untangle the AST and CFG hierarchies.
*
* Gets a flow node corresponding directly to this node.
* NOTE: For some statements and other purely syntactic elements,
* there may not be a `ControlFlowNode`.
* Gets a flow node corresponding directly to this node, from the new
* (shared) CFG. NOTE: For some statements and other purely syntactic
* elements, there may not be a `ControlFlowNode`.
*/
cached
deprecated ControlFlowNode getAFlowNode() {
deprecated Cfg::ControlFlowNode getAFlowNode() {
Stages::AST::ref() and
py_flow_bb_node(result, this, _, _)
result.getNode() = this
}

/** Gets the location for this AST node */
Expand Down
5 changes: 3 additions & 2 deletions python/ql/lib/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

private import python
private import semmle.python.controlflow.internal.Cfg as Cfg
private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.internal.DataFlowImplSpecific
private import semmle.python.dataflow.new.RemoteFlowSources
Expand Down Expand Up @@ -214,7 +215,7 @@ module Path {
SafeAccessCheck() { this = DataFlow::BarrierGuard<safeAccessCheck/3>::getABarrierNode() }
}

private predicate safeAccessCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
private predicate safeAccessCheck(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) {
g.(SafeAccessCheck::Range).checks(node, branch)
}

Expand All @@ -223,7 +224,7 @@ module Path {
/** A data-flow node that checks that a path is safe to access in some way, for example by having a controlled prefix. */
abstract class Range extends DataFlow::GuardNode {
/** Holds if this guard validates `node` upon evaluating to `branch`. */
abstract predicate checks(ControlFlowNode node, boolean branch);
abstract predicate checks(Cfg::ControlFlowNode node, boolean branch);
}
}
}
Expand Down
15 changes: 8 additions & 7 deletions python/ql/lib/semmle/python/Exprs.qll
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module;

private import python
private import semmle.python.internal.CachedStages
private import semmle.python.controlflow.internal.Cfg as Cfg

/** An expression */
class Expr extends Expr_, AstNode {
Expand Down Expand Up @@ -70,7 +71,7 @@ class Attribute extends Attribute_ {
/* syntax: Expr.name */
override Expr getASubExpression() { result = this.getObject() }

deprecated override AttrNode getAFlowNode() { result = super.getAFlowNode() }
deprecated override Cfg::AttrNode getAFlowNode() { result = super.getAFlowNode() }

/** Gets the name of this attribute. That is the `name` in `obj.name` */
string getName() { result = Attribute_.super.getAttr() }
Expand Down Expand Up @@ -99,7 +100,7 @@ class Subscript extends Subscript_ {

Expr getObject() { result = Subscript_.super.getValue() }

deprecated override SubscriptNode getAFlowNode() { result = super.getAFlowNode() }
deprecated override Cfg::SubscriptNode getAFlowNode() { result = super.getAFlowNode() }
}

/** A call expression, such as `func(...)` */
Expand All @@ -115,7 +116,7 @@ class Call extends Call_ {

override string toString() { result = this.getFunc().toString() + "()" }

deprecated override CallNode getAFlowNode() { result = super.getAFlowNode() }
deprecated override Cfg::CallNode getAFlowNode() { result = super.getAFlowNode() }

/** Gets a tuple (*) argument of this call. */
Expr getStarargs() { result = this.getAPositionalArg().(Starred).getValue() }
Expand Down Expand Up @@ -203,7 +204,7 @@ class IfExp extends IfExp_ {
result = this.getTest() or result = this.getBody() or result = this.getOrelse()
}

deprecated override IfExprNode getAFlowNode() { result = super.getAFlowNode() }
deprecated override Cfg::IfExprNode getAFlowNode() { result = super.getAFlowNode() }
}

/** A starred expression, such as the `*rest` in the assignment `first, *rest = seq` */
Expand Down Expand Up @@ -413,7 +414,7 @@ class PlaceHolder extends PlaceHolder_ {

override string toString() { result = "$" + this.getId() }

deprecated override NameNode getAFlowNode() { result = super.getAFlowNode() }
deprecated override Cfg::NameNode getAFlowNode() { result = super.getAFlowNode() }
}

/** A tuple expression such as `( 1, 3, 5, 7, 9 )` */
Expand Down Expand Up @@ -480,7 +481,7 @@ class Name extends Name_ {

override string toString() { result = this.getId() }

deprecated override NameNode getAFlowNode() { result = super.getAFlowNode() }
deprecated override Cfg::NameNode getAFlowNode() { result = super.getAFlowNode() }

override predicate isArtificial() {
/* Artificial variable names in comprehensions all start with "." */
Expand Down Expand Up @@ -587,7 +588,7 @@ abstract class NameConstant extends Name, ImmutableLiteral {

override predicate isConstant() { any() }

deprecated override NameConstantNode getAFlowNode() { result = Name.super.getAFlowNode() }
deprecated override Cfg::NameConstantNode getAFlowNode() { result = Name.super.getAFlowNode() }

override predicate isArtificial() { none() }
}
Expand Down
20 changes: 3 additions & 17 deletions python/ql/lib/semmle/python/Flow.qll
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ class ControlFlowNode extends @py_flow_node {
/** Gets a textual representation of this element. */
cached
string toString() {
Stages::AST::ref() and
// Since modules can have ambigous names, entry nodes can too, if we do not collate them.
exists(Py::Scope s | s.getEntryNode() = this |
result = "Entry node for " + concat( | | s.toString(), ",")
Expand All @@ -152,7 +151,6 @@ class ControlFlowNode extends @py_flow_node {
/** Gets the scope containing this flow node */
cached
Py::Scope getScope() {
Stages::AST::ref() and
if this.getNode() instanceof Py::Scope
then
/* Entry or exit node */
Expand Down Expand Up @@ -554,7 +552,6 @@ class UnaryExprNode extends ControlFlowNode {
class DefinitionNode extends ControlFlowNode {
cached
DefinitionNode() {
Stages::AST::ref() and
exists(Py::Assign a | this.getNode() = a.getATarget())
or
exists(Py::AssignExpr a | this.getNode() = a.getTarget())
Expand Down Expand Up @@ -635,7 +632,6 @@ class TupleNode extends SequenceNode {
TupleNode() { toAst(this) instanceof Py::Tuple }

override ControlFlowNode getElement(int n) {
Stages::AST::ref() and
exists(Py::Tuple t | this.getNode() = t and result.getNode() = t.getElt(n)) and
(
result.getBasicBlock().dominates(this.getBasicBlock())
Expand Down Expand Up @@ -1015,10 +1011,7 @@ class BasicBlock extends @py_flow_node {

/** Whether this basic block strictly dominates the other */
cached
predicate strictlyDominates(BasicBlock other) {
Stages::AST::ref() and
other.getImmediateDominator+() = this
}
predicate strictlyDominates(BasicBlock other) { other.getImmediateDominator+() = this }

/** Whether this basic block dominates the other */
predicate dominates(BasicBlock other) {
Expand All @@ -1029,7 +1022,6 @@ class BasicBlock extends @py_flow_node {

cached
BasicBlock getImmediateDominator() {
Stages::AST::ref() and
this.firstNode().getImmediateDominator().getBasicBlock() = result
}

Expand Down Expand Up @@ -1075,10 +1067,7 @@ class BasicBlock extends @py_flow_node {

/** Gets a successor to this basic block */
cached
BasicBlock getASuccessor() {
Stages::AST::ref() and
result = this.getLastNode().getASuccessor().getBasicBlock()
}
BasicBlock getASuccessor() { result = this.getLastNode().getASuccessor().getBasicBlock() }

/** Gets a predecessor to this basic block */
BasicBlock getAPredecessor() { result.getASuccessor() = this }
Expand Down Expand Up @@ -1140,10 +1129,7 @@ class BasicBlock extends @py_flow_node {

/** Holds if this basic block strictly reaches the other. Is the start of other reachable from the end of this. */
cached
predicate strictlyReaches(BasicBlock other) {
Stages::AST::ref() and
this.getASuccessor+() = other
}
predicate strictlyReaches(BasicBlock other) { this.getASuccessor+() = other }

/** Holds if this basic block reaches the other. Is the start of other reachable from the end of this. */
predicate reaches(BasicBlock other) { this = other or this.strictlyReaches(other) }
Expand Down
9 changes: 5 additions & 4 deletions python/ql/lib/semmle/python/Function.qll
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ overlay[local]
module;

import python
private import semmle.python.controlflow.internal.Cfg as Cfg

/**
* A function, independent of defaults and binding.
Expand Down Expand Up @@ -157,12 +158,12 @@ class Function extends Function_, Scope, AstNode {
* DEPRECATED: bind a `Return` node explicitly instead, e.g.
* `exists(Return ret | ret.getScope() = this and n.getNode() = ret.getValue())`.
* This API is being phased out together with `AstNode.getAFlowNode()` to
* untangle the AST and CFG hierarchies in preparation for migrating the
* dataflow library off the legacy CFG.
* untangle the AST and CFG hierarchies.
*
* Gets a control flow node for a return value of this function.
* Gets a control flow node for a return value of this function, from the
* new (shared) CFG.
*/
deprecated ControlFlowNode getAReturnValueFlowNode() {
deprecated Cfg::ControlFlowNode getAReturnValueFlowNode() {
exists(Return ret |
ret.getScope() = this and
ret.getValue() = result.getNode()
Expand Down
3 changes: 2 additions & 1 deletion python/ql/lib/semmle/python/Import.qll
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module;
import python
private import semmle.python.types.Builtins
private import semmle.python.internal.CachedStages
private import semmle.python.controlflow.internal.Cfg as Cfg

/**
* An alias in an import statement, the `mod as name` part of `import mod as name`. May be artificial;
Expand Down Expand Up @@ -163,7 +164,7 @@ class ImportMember extends ImportMember_ {
result = this.getModule().(ImportExpr).getImportedModuleName() + "." + this.getName()
}

deprecated override ImportMemberNode getAFlowNode() { result = super.getAFlowNode() }
deprecated override Cfg::ImportMemberNode getAFlowNode() { result = super.getAFlowNode() }
}

/** An import statement */
Expand Down
9 changes: 7 additions & 2 deletions python/ql/lib/semmle/python/controlflow/internal/Cfg.qll
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ private import codeql.controlflow.SuccessorType
*/
class ControlFlowNode extends CfgImpl::ControlFlowNode {
/** Gets the syntactic element corresponding to this flow node, if any. */
cached
Py::AstNode getNode() {
exists(CfgImpl::Ast::AstNode n | this.injects(n) | result = CfgImpl::astNodeToPyNode(n))
}

/** Gets the Python expression corresponding to this flow node, if any. */
Py::Expr asPyExpr() { result = this.getNode() }

/** Gets a predecessor of this flow node. */
Expand Down Expand Up @@ -67,6 +69,7 @@ class ControlFlowNode extends CfgImpl::ControlFlowNode {
BasicBlock getBasicBlock() { result = super.getBasicBlock() }

/** Gets the scope containing this flow node. */
cached
Py::Scope getScope() { result = super.getEnclosingCallable().asScope() }

/** Gets the enclosing module. */
Expand All @@ -85,13 +88,15 @@ class ControlFlowNode extends CfgImpl::ControlFlowNode {
}

/** Holds if this strictly dominates `other`. */
bindingset[this, other]
overlay[caller?]
pragma[inline]
pragma[inline_late]
predicate strictlyDominates(ControlFlowNode other) { super.strictlyDominates(other) }

/** Holds if this dominates `other` (reflexively). */
bindingset[this, other]
overlay[caller?]
pragma[inline]
pragma[inline_late]
predicate dominates(ControlFlowNode other) { super.dominates(other) }

/** Holds if this is the first node in its enclosing scope. */
Expand Down
Loading
Loading