Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
593e17a
unified: Add some tests with captured vars
asgerf Sep 29, 2026
d08e0b9
unified: Split Node into "stage 1" and "final stage"
asgerf Sep 29, 2026
2f46c31
unified: Factor out LocalVariableRefNode
asgerf Sep 29, 2026
6d4e275
unified: Add canonical callable node
asgerf Sep 29, 2026
d76cdb3
unified: Exclude function types from DataFlowCallable
asgerf Sep 29, 2026
607010a
unified: Function declaration names belong to the outer scope
asgerf Sep 29, 2026
f736b03
unified: Instantiate capture lib and enable consistency queries
asgerf Sep 29, 2026
ca6a6a8
unified: Add Stage2Node
asgerf Sep 29, 2026
058d767
unified: Use local data flow for alias detection
asgerf Sep 29, 2026
8e5b2c6
unified: Add TCaptureSsaNode
asgerf Sep 30, 2026
0ea037b
unified: Add callee param/arg position
asgerf Sep 30, 2026
0e84147
unified: Callee always has a post-update
asgerf Sep 30, 2026
bd1c743
unified: Generalize receiver nodes to implicit arg/param nodes
asgerf Sep 30, 2026
c26ebf9
unified: Add convenience getters
asgerf Sep 30, 2026
f3be2ce
unified: Mapping capture nodes to data flow nodes
asgerf Sep 30, 2026
c8ae4d3
unified: Add CapturedVariable content
asgerf Sep 30, 2026
b0c2d54
unified: Add CaptureSsa steps and post-updates
asgerf Sep 30, 2026
7e6bd55
unified: Update debug graph
asgerf Sep 30, 2026
d7130c2
unified: Add lambda flow
asgerf Sep 30, 2026
6e0a3b7
unified: Add some tests with local function declarations
asgerf Sep 30, 2026
7762b17
unified: Add test with local function used before its declaration
asgerf Sep 30, 2026
343fe76
unified: Hoist local functions
asgerf Sep 30, 2026
68d7e3e
unified: Add tests with capture declarations
asgerf Sep 30, 2026
3321c32
unified: Desugar capture declaration list
asgerf Sep 30, 2026
7b462e4
fixup! dont add unary_expr
asgerf Oct 14, 2026
f30f6c9
unified: Update test case
asgerf Sep 30, 2026
6533df1
unified: Fix scoping for capture declarations
asgerf Oct 1, 2026
4afec0c
unified: Simplify with getParentIndex
asgerf Oct 1, 2026
d0a38cb
Shared: Force join order in VariableCapture
asgerf Oct 1, 2026
7f9ebee
unified: Remove blank lines from expected file
asgerf Oct 8, 2026
f3afa24
unified: Update test after rebasing
asgerf Oct 14, 2026
d82fc83
unified: Add tests with 'unowned' captures
asgerf Oct 8, 2026
f50c74c
unified: Auto-format self_access.swift
asgerf Oct 9, 2026
1b94078
unified: Fix variable not being used in all disjuncts
asgerf Oct 9, 2026
1ab8fcd
unified: Avoid spurious recursion in toString and getLocation
asgerf Oct 15, 2026
cd6386a
unified: Add TODO about escape analysis
asgerf Oct 15, 2026
31488c0
unified: Elaborate on CapturedParameter
asgerf Oct 15, 2026
4be4ff5
unified: Add TODO in ClosureExpr
asgerf Oct 15, 2026
48e3e11
unified: Rename get -> to
asgerf Oct 15, 2026
10853e6
unified: Preserve TODO at clearsContent
asgerf Oct 15, 2026
f666d7e
unified: Autoformat
asgerf Oct 15, 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 @@ -483,6 +483,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 @@ -496,6 +497,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 @@ -512,7 +520,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 @@ -812,6 +820,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
14 changes: 9 additions & 5 deletions unified/extractor/src/languages/swift/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,10 +823,8 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
return_type: {ret}
body: (block stmt: {body}))
),
// A closure capture (`[weak self]`, `[x]`, `[y = expr]`). The optional
// ownership specifier (`weak`/`unowned`) becomes a modifier; the
// captured name becomes the bound `name_node`; an explicit capture
// initializer (`[y = expr]`) becomes the bound value.
// A closure capture (`[weak self]`, `[x]`, `[y = expr]`).
// Normalize so there is always an explicit initializer, expanding `[x]` into `[x = x]`.
rule!(
(closureCapture
specifier: (closureCaptureSpecifier specifier: @@spec)?
Expand All @@ -836,7 +834,13 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
(variable_declaration
modifier: (modifier #{spec})?
pattern: (identifier #{name})
value: {val})
value: {
// Expand [x] into [x = x]
match val {
Some(val) => val,
None => tree!((identifier #{name})),
}
Comment on lines +839 to +842

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use unwrap_or_else.

Suggested change
match val {
Some(val) => val,
None => tree!((identifier #{name})),
}
val.unwrap_or_else(|| tree!((identifier #{name})))

})
),
// 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,10 @@ top_level source="⟨body⟩"
value:
function_expr source="{ [⟨capture_declaration⟩] in ⟨body⟩ }"
capture_declaration:
variable_declaration source="⟨modifier⟩ ⟨pattern⟩"
variable_declaration source="⟨modifier⟩ ⟨pattern⟩⟨value⟩"
modifier: modifier "weak" source="weak"
pattern: identifier "self" source="self"
value: identifier "self" source="self"
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 @@ -43,6 +43,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 @@ -54,6 +54,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 @@ -72,35 +72,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 @@ -123,7 +98,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 @@ -176,11 +151,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 ConstructorPatterns
import DataFlowCall
Expand All @@ -14,3 +15,4 @@ import ParameterPositions
import Step
import TaintTrackingInstantiation
import VariableRefKind
import LocalVariableRefNode
180 changes: 180 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,180 @@
/**
* 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 should be empty, as the capture library never gets to work with a node that corresponds to a `DataFlowInput::ParameterNode`.
//
// When a variable is mentioned in a pattern in the parameter list, the pattern itself (TValueNode) becomes the ParameterNode
// and the assignment to the variable is modeled as an ordinary variable-assignment at the point where the variable mention appears in the CFG.
// Likewise, the receiver is represented by TImplicitParameterNode and its assignment to 'self/this' is modeled as an ordinary assignment.
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.(BuilderNode).toString() }

Location getLocation() { result = this.(BuilderNode).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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This equates closure expressions and callables. I'm not sure if this happens in Swift, but that won't work in general. Some languages allow the construction of closure objects with multiple methods, e.g. anonymous class instances in Java (of which a Java lambda is but one example).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A local class definition is another example where capture might happen in an object with multiple methods (again, I'm not sure which languages this applies to, but I know it's valid Java).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, I'll add a TODO. Swift local classes can't capture variables so we can't exercise this feature with Swift.

// TODO: In order to support classes with captures, this needs to cover constructor calls to such classes as well.
private Callable callable;

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

predicate hasBody(Callable body) { callable = body }

predicate hasAliasedAccess(Expr f) {
// TODO: Restrict to places where the function is called or escapes into a call where it might get called.
callableHasLocalAlias(callable, f) and not f.(LocalVariableRefNode).getRefKind().isWrite()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relation was not meant to be reflexive. Also, it was not meant to include all the aliases - just the ones that might escape or represent an invocation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave a TODO for now. It's easy to accidentally make something too under-approximate and not notice because FNs are hard to detect. I want to over-approximate this first and defer escape analysis until we are in a better position to measure its correctness at scale.

}
}
}

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(toDataFlowCallable(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
))
)
}
11 changes: 9 additions & 2 deletions unified/ql/lib/codeql/unified/internal/dataflow/Content.qll
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ private newtype TContent =
name = [0 .. 20].toString()
or
name = getEnumCaseParameterFieldFromArgument(_, _)
}
} or
TCapturedVariable(CaptureSsaInput::CapturedVariable v)

class Content extends TContent {
string asNamedMember() { this = TNamedMember(result) }
Expand All @@ -22,9 +23,13 @@ class Content extends TContent {
result = this.asNamedMember()
or
this.isArrayElement() and result = "ArrayElement"
or
result = this.asCapturedVariable().toString()
}

Location getLocation() { none() }
LocalVariable asCapturedVariable() { this = TCapturedVariable(result) }

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

private newtype TContentSet = TSingleton(Content content)
Expand All @@ -45,4 +50,6 @@ module ContentSet {
ContentSet namedMember(string name) { result.asSingleton().asNamedMember() = name }

ContentSet arrayElement() { result.asSingleton().isArrayElement() }

ContentSet capturedVariable(LocalVariable v) { result.asSingleton().asCapturedVariable() = v }
}
Loading
Loading