Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a8adb25
unified: Add some tests with captured vars
asgerf Sep 29, 2026
3a834fe
unified: Split Node into "stage 1" and "final stage"
asgerf Sep 29, 2026
ee39510
unified: Factor out LocalVariableRefNode
asgerf Sep 29, 2026
d6399da
unified: Add canonical callable node
asgerf Sep 29, 2026
7542cf7
unified: Exclude function types from DataFlowCallable
asgerf Sep 29, 2026
98e4076
fixup some downcasts
asgerf Oct 1, 2026
1fbdba4
unified: Function declaration names belong to the outer scope
asgerf Sep 29, 2026
8803aeb
unified: Instantiate capture lib and enable consistency queries
asgerf Sep 29, 2026
b097674
unified: Add Stage2Node
asgerf Sep 29, 2026
814b284
unified: Use local data flow for alias detection
asgerf Sep 29, 2026
567f0ea
unified: Add TCaptureSsaNode
asgerf Sep 30, 2026
d0bdb29
unified: Add callee param/arg position
asgerf Sep 30, 2026
5578337
unified: Callee always has a post-update
asgerf Sep 30, 2026
a2bff0b
unified: Generalize receiver nodes to implicit arg/param nodes
asgerf Sep 30, 2026
4445b2b
unified: Add convenience getters
asgerf Sep 30, 2026
3bd30c8
unified: Mapping capture nodes to data flow nodes
asgerf Sep 30, 2026
447268b
unified: Add CapturedVariable content
asgerf Sep 30, 2026
9813593
unified: Add CaptureSsa steps and post-updates
asgerf Sep 30, 2026
782ed06
unified: Update debug graph
asgerf Sep 30, 2026
50268c6
unified: Add lambda flow
asgerf Sep 30, 2026
26d3adf
unified: Add some tests with local function declarations
asgerf Sep 30, 2026
ff01810
unified: Add test with local function used before its declaration
asgerf Sep 30, 2026
6160807
unified: Hoist local functions
asgerf Sep 30, 2026
c153b6c
unified: Add tests with capture declarations
asgerf Sep 30, 2026
1a1d449
unified: Desugar capture declaration list
asgerf Sep 30, 2026
d0c8f67
unified: Update test case
asgerf Sep 30, 2026
4e1adf0
unified: Fix scoping for capture declarations
asgerf Oct 1, 2026
d0cd3c5
unified: Simplify with getParentIndex
asgerf Oct 1, 2026
753f0d4
Shared: Force join order in VariableCapture
asgerf Oct 1, 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
20 changes: 19 additions & 1 deletion shared/dataflow/codeql/dataflow/VariableCapture.qll
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ module Flow<
}

/** Gets the enclosing callable of `ce`. */
pragma[nomagic]
private Callable closureExprGetEnclosingCallable(ClosureExpr ce) {
exists(BasicBlock bb | ce.hasCfgNode(bb, _) and result = bb.getEnclosingCallable())
}
Expand All @@ -494,6 +495,13 @@ module Flow<
)
}

/** Holds if `outer` contains or equals `inner` */
bindingset[outer, inner]
pragma[inline_late]
private predicate isEnclosingCallable(Callable outer, Callable inner) {
outer = callableGetEnclosingCallable*(inner)
}

/**
* Gets a callable that contains `ce`, or a reference to `ce` into which `ce` could be inlined without
* bringing any variables out of scope.
Expand All @@ -510,7 +518,7 @@ module Flow<
expr.hasCfgNode(bb, _) and
result = bb.getEnclosingCallable() and
// The reference to `ce` is allowed to occur in a more deeply nested context
closureExprGetEnclosingCallable(ce) = callableGetEnclosingCallable*(result)
isEnclosingCallable(closureExprGetEnclosingCallable(ce), result)
)
}

Expand Down Expand Up @@ -810,6 +818,16 @@ module Flow<
or
exists(SsaFlow::SsaNode n | this = TSynthSsa(n) and n.getSourceVariable() = TThis(_))
}

predicate hasCfgNode(BasicBlock bb, int i) {
this = TSynthRead(_, bb, i, _)
or
this = TSynthThisQualifier(bb, i, _)
or
exists(SsaFlow::SsaNode n |
this = TSynthSsa(n) and n.getBasicBlock() = bb and n.getIndex() = i
)
}
}

class ExprNode extends ClosureNode, TExprNode {
Expand Down
15 changes: 13 additions & 2 deletions unified/extractor/src/languages/swift/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
body: (block stmt: {body}))
),
// A closure capture (`[weak self]`, `[x]`, `[y = expr]`). The optional
// ownership specifier (`weak`/`unowned`) becomes a modifier; the
// ownership specifier (`weak`/`unowned`) becomes a modifier and a unary_expr; the
// captured name becomes the bound `name_node`; an explicit capture
// initializer (`[y = expr]`) becomes the bound value.
rule!(
Expand All @@ -790,7 +790,18 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
(variable_declaration
modifier: (modifier #{spec})?
pattern: (identifier #{name})
value: {val})
value: {
// Expand [x] into [x = x]
let value = match val {
Some(val) => val,
None => tree!((identifier #{name})),
};
// Expand [weak x] into a unary_expr, to represent the boxing in Optional.same
match spec {
Some(spec) => tree!((unary_expr operator: (prefix_operator #{spec}) operand: {value})),
None => value,
}
})
),
// A closure parameter clause (`(x: Int, y)`) unwraps to its parameters.
rule!((closureParameterClause parameters: _* @params) => parameter* { params }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,13 @@ top_level source="⟨body⟩"
value:
function_expr source="{ [⟨capture_declaration⟩] in ⟨body⟩ }"
capture_declaration:
variable_declaration source="⟨modifier⟩ ⟨pattern⟩"
variable_declaration source="⟨modifier⟩⟨value⟩⟨pattern⟩"
modifier: modifier "weak" source="weak"
pattern: identifier "self" source="self"
value:
unary_expr source="⟨operator⟩ ⟨operand⟩"
operand: identifier "self" source="self"
operator: prefix_operator "weak" source="weak"
body:
block source="⟨stmt⟩"
stmt:
Expand Down
3 changes: 3 additions & 0 deletions unified/ql/consistency-queries/CaptureSsaConsistency.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
private import unified
private import codeql.unified.internal.dataflow.CaptureSsa
import CaptureSsaOutput::ConsistencyChecks
2 changes: 2 additions & 0 deletions unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ module Ast implements AstSig<Location> {
not skipControlFlow(result)
or
n.(FunctionExpr).getCaptureDeclaration(index) = result
or
n.(FunctionDeclaration).getNameNode() = result and index = 0
}

Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() }
Expand Down
6 changes: 6 additions & 0 deletions unified/ql/lib/codeql/unified/internal/FacadeAst.qll
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ module Unified {
this = func.getACaptureDeclaration() and
result = func.getParent()
)
or
exists(FunctionDeclaration func |
// The name of a function declaration is a variable belonging to the enclosing callable.
this = func.getNameNode() and
result = func.getParent()
)
}

/**
Expand Down
32 changes: 1 addition & 31 deletions unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll
Original file line number Diff line number Diff line change
Expand Up @@ -70,35 +70,10 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig<Locatio
}

private AstNode getChild1(AstNode n, int index) {
result = n.(Block).getStmt(index) and
not n instanceof BlockWithGuardStmts
or
result = n.(BlockWithGuardStmts).getTranslatedChild(index)
or
result = n.(LogicalAndRoot).getNthLeaf(index)
or
exists(PatternGuardExpr guard | n = guard |
index = 0 and result = guard.getPattern()
or
index = 1 and result = guard.getValue()
)
or
exists(IfExpr expr | n = expr |
index = 0 and result = expr.getCondition()
or
index = 1 and result = expr.getThen()
or
index = 2 and result = expr.getElse()
)
or
exists(VariableDeclaration decl | n = decl |
index = 0 and result = decl.getPattern()
or
index = 1 and result = decl.getType()
or
index = 2 and result = decl.getValue()
)
or
index = 0 and
relocatedClassMember(n, result)
}
Expand All @@ -121,7 +96,7 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig<Locatio
not n instanceof LogicalAndExpr and // also ignore intermediate nodes within a 'logical and' tree
not n instanceof GuardIfStmt and
not relocatedClassMember(_, result) and
index = 0 and
index = result.getParentIndex() and
result = n.getAFieldOrChild()
}

Expand Down Expand Up @@ -174,11 +149,6 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig<Locatio

private class LocalVariableDeclarationSiblingShadowingDecl extends SiblingShadowingDecl instanceof LocalVariableDeclaration
{
LocalVariableDeclarationSiblingShadowingDecl() {
// Capture-declarations act as local variables, but are not sibling-shadowing
not this = any(FunctionExpr e).getACaptureDeclaration()
}

override Expr getPattern() { result = LocalVariableDeclaration.super.getPattern() }

override AstNode getRhs() { result = LocalVariableDeclaration.super.getValue() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Re-exports all the files in the internal dataflow folder (except DataFlowPublic). */

import CallGraph
import CaptureSsa
import Content
import DataFlowCall
import DataFlowCallable
Expand All @@ -13,3 +14,4 @@ import ParameterPositions
import Step
import TaintTrackingInstantiation
import VariableRefKind
import LocalVariableRefNode
174 changes: 174 additions & 0 deletions unified/ql/lib/codeql/unified/internal/dataflow/CaptureSsa.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* SSA for captured variables.
*/

private import unified
private import unified as U
private import AllDataFlow
private import codeql.dataflow.VariableCapture
private import codeql.unified.internal.ControlFlowGraph
private import codeql.unified.internal.ExprPositions

module CaptureSsaInput implements InputSig<Location, BasicBlock> {
final private class FinalCallable = U::Callable;

class Callable extends FinalCallable {
Callable() { this.fromSource() }

predicate isConstructor() { none() } // TODO
}

Callable basicBlockGetEnclosingCallable(BasicBlock bb) { result = bb.getEnclosingCallable() }

class CapturedVariable extends LocalVariable {
CapturedVariable() { this.isCaptured() }

Callable getCallable() { result = super.getDeclaringCallable() }
}

class CapturedParameter extends CapturedVariable {
// This class is not needed. Variables aren't the first point of contact with parameter values.
CapturedParameter() { none() }
}

private class TExpr = TCallableNode or TLocalVariableRefNode;

private predicate isTopLevelCallable(BuilderNode callable) {
callable.isCallable(any(TopLevel t))
}

class Expr extends TExpr {
Expr() { not isTopLevelCallable(this) }

string toString() { result = this.(Node).toString() }

Location getLocation() { result = this.(Node).getLocation() }

predicate hasCfgNode(BasicBlock bb, int i) {
this.(LocalVariableRefNode).hasCfgNode(bb, i)
or
exists(DataFlowCallable callable |
this = TCallableNode(callable) and
bb.getNode(i).injects(callable.asSourceCallable())
)
}
}

final private class FinalLocalVariableRefNode = LocalVariableRefNode;

class VariableWrite extends FinalLocalVariableRefNode {
VariableWrite() {
this.getRefKind().isWrite() and super.getVariable() instanceof CapturedVariable
}

CapturedVariable getVariable() { result = super.getVariable() }
}

class VariableRead extends Expr instanceof LocalVariableRefNode {
VariableRead() {
super.getVariable() instanceof CapturedVariable and
super.getRefKind().isRead()
}

CapturedVariable getVariable() { result = super.getVariable() }
}

/**
* Holds if `node` is a possible alias for `callable`.
*/
private predicate callableHasLocalAlias(Callable callable, Stage2Node node) {
node.(BuilderNode).isCallable(callable)
or
exists(Stage2Node prev | callableHasLocalAlias(callable, prev) |
step(prev, any(Step s | s.value()), node)
or
localSsaStep(prev, node, _)
)
or
exists(CapturedVariable var |
callableHasLocalAliasVar(callable, var) and
node.(BuilderNode).isLocalVariableRead(_, var)
)
}

pragma[nomagic]
private predicate callableHasLocalAliasVar(Callable callable, CapturedVariable var) {
exists(BuilderNode ref |
callableHasLocalAlias(callable, ref) and
ref.isLocalVariableWrite(_, var)
)
}

class ClosureExpr extends Expr instanceof TCallableNode {
private Callable callable;

ClosureExpr() { this.(BuilderNode).isCallable(callable) }

predicate hasBody(Callable body) { callable = body }

predicate hasAliasedAccess(Expr f) {
callableHasLocalAlias(callable, f) and not f.(LocalVariableRefNode).getRefKind().isWrite()
}
}
}

module CaptureSsaOutput = Flow<Location, Cfg, CaptureSsaInput>;

Node getNodeFromCaptureSsaNode(CaptureSsaOutput::ClosureNode n) {
result = TCaptureSsaNode(n)
or
result = n.(CaptureSsaOutput::ExprNode).getExpr()
or
result = n.(CaptureSsaOutput::ExprPostUpdateNode).getExpr().(BuilderNode).getPostUpdateNode()
or
result = n.(CaptureSsaOutput::VariableWriteSourceNode).getVariableWrite()
or
// NOTE: This only supports lambdas at the moment. Local classes in Swift cannot capture variables.
result = n.(CaptureSsaOutput::MallocNode).getClosureExpr()
or
exists(CaptureSsaOutput::ThisParameterNode thisParam, Callable callable |
n = thisParam and
callable = thisParam.getCallable() and
result
.(BuilderNode)
.isImplicitParameter(getDataFlowCallable(callable), any(ParameterPosition p | p.isCallee()))
)
}

CaptureSsaOutput::ClosureNode getCaptureSsaNodeFromNode(Node n) {
n = getNodeFromCaptureSsaNode(result)
}

predicate captureSsaLocalFlowStep(Node node1, Node node2) {
CaptureSsaOutput::localFlowStep(getCaptureSsaNodeFromNode(node1), getCaptureSsaNodeFromNode(node2))
}

predicate captureSsaStoreStep(Node node1, ContentSet contents, Node node2) {
CaptureSsaOutput::storeStep(getCaptureSsaNodeFromNode(node1),
contents.asSingleton().asCapturedVariable(), getCaptureSsaNodeFromNode(node2))
}

predicate captureSsaReadStep(Node node1, ContentSet contents, Node node2) {
CaptureSsaOutput::readStep(getCaptureSsaNodeFromNode(node1),
contents.asSingleton().asCapturedVariable(), getCaptureSsaNodeFromNode(node2))
}

predicate captureSsaClearsContent(Node node, ContentSet contents) {
CaptureSsaOutput::clearsContent(getCaptureSsaNodeFromNode(node),
contents.asSingleton().asCapturedVariable())
}

Node getCaptureSsaPostUpdate(Node pre) {
CaptureSsaOutput::capturePostUpdateNode(getCaptureSsaNodeFromNode(result),
getCaptureSsaNodeFromNode(pre))
}

predicate captureSsaAllowParameterReturnInSelf(Node param) {
exists(Callable callable |
CaptureSsaOutput::heuristicAllowInstanceParameterReturnInSelf(callable) and
param =
getNodeFromCaptureSsaNode(any(CaptureSsaOutput::ThisParameterNode n |
n.getCallable() = callable
))
)
}
13 changes: 10 additions & 3 deletions unified/ql/lib/codeql/unified/internal/dataflow/Content.qll
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ private newtype TContent =
// Tuple elements can be accessed as named members, e.g. `tuple.0`, `tuple.1`, etc,
// so just model their elements as named members.
name = [0 .. 20].toString()
}
} or
TCapturedVariable(CaptureSsaInput::CapturedVariable v)

class Content extends TContent {
string asNamedMember() { this = TNamedMember(result) }

string toString() { result = this.asNamedMember() }
string toString() {
result = this.asNamedMember() or result = this.asCapturedVariable().toString()
}

LocalVariable asCapturedVariable() { this = TCapturedVariable(result) }

Location getLocation() { none() }
Location getLocation() { result = this.asCapturedVariable().getLocation() }
}

private newtype TContentSet = TSingleton(Content content)
Expand All @@ -34,4 +39,6 @@ class ContentSet extends TContentSet {

module ContentSet {
ContentSet namedMember(string name) { result.asSingleton().asNamedMember() = name }

ContentSet capturedVariable(LocalVariable v) { result.asSingleton().asCapturedVariable() = v }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ class DataFlowCall extends TDataFlowCall {
result.asSourceCallable() = this.asExplicitCall().getEnclosingCallable()
}
}

DataFlowCall getDataFlowCall(CallExpr call) { result.asExplicitCall() = call }
Loading
Loading