Skip to content

Commit 0c40f4d

Browse files
authored
Merge pull request #22726 from asgerf/unified/captured-variables
Unified: captured variables
2 parents 2ed987f + f666d7e commit 0c40f4d

27 files changed

Lines changed: 1097 additions & 243 deletions

‎shared/dataflow/codeql/dataflow/VariableCapture.qll‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ module Flow<
483483
}
484484

485485
/** Gets the enclosing callable of `ce`. */
486+
pragma[nomagic]
486487
private Callable closureExprGetEnclosingCallable(ClosureExpr ce) {
487488
exists(BasicBlock bb | ce.hasCfgNode(bb, _) and result = bb.getEnclosingCallable())
488489
}
@@ -496,6 +497,13 @@ module Flow<
496497
)
497498
}
498499

500+
/** Holds if `outer` contains or equals `inner` */
501+
bindingset[outer, inner]
502+
pragma[inline_late]
503+
private predicate isEnclosingCallable(Callable outer, Callable inner) {
504+
outer = callableGetEnclosingCallable*(inner)
505+
}
506+
499507
/**
500508
* Gets a callable that contains `ce`, or a reference to `ce` into which `ce` could be inlined without
501509
* bringing any variables out of scope.
@@ -512,7 +520,7 @@ module Flow<
512520
expr.hasCfgNode(bb, _) and
513521
result = bb.getEnclosingCallable() and
514522
// The reference to `ce` is allowed to occur in a more deeply nested context
515-
closureExprGetEnclosingCallable(ce) = callableGetEnclosingCallable*(result)
523+
isEnclosingCallable(closureExprGetEnclosingCallable(ce), result)
516524
)
517525
}
518526

@@ -812,6 +820,16 @@ module Flow<
812820
or
813821
exists(SsaFlow::SsaNode n | this = TSynthSsa(n) and n.getSourceVariable() = TThis(_))
814822
}
823+
824+
predicate hasCfgNode(BasicBlock bb, int i) {
825+
this = TSynthRead(_, bb, i, _)
826+
or
827+
this = TSynthThisQualifier(bb, i, _)
828+
or
829+
exists(SsaFlow::SsaNode n |
830+
this = TSynthSsa(n) and n.getBasicBlock() = bb and n.getIndex() = i
831+
)
832+
}
815833
}
816834

817835
class ExprNode extends ClosureNode, TExprNode {

‎unified/extractor/src/languages/swift/swift.rs‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -823,10 +823,8 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
823823
return_type: {ret}
824824
body: (block stmt: {body}))
825825
),
826-
// A closure capture (`[weak self]`, `[x]`, `[y = expr]`). The optional
827-
// ownership specifier (`weak`/`unowned`) becomes a modifier; the
828-
// captured name becomes the bound `name_node`; an explicit capture
829-
// initializer (`[y = expr]`) becomes the bound value.
826+
// A closure capture (`[weak self]`, `[x]`, `[y = expr]`).
827+
// Normalize so there is always an explicit initializer, expanding `[x]` into `[x = x]`.
830828
rule!(
831829
(closureCapture
832830
specifier: (closureCaptureSpecifier specifier: @@spec)?
@@ -836,7 +834,13 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
836834
(variable_declaration
837835
modifier: (modifier #{spec})?
838836
pattern: (identifier #{name})
839-
value: {val})
837+
value: {
838+
// Expand [x] into [x = x]
839+
match val {
840+
Some(val) => val,
841+
None => tree!((identifier #{name})),
842+
}
843+
})
840844
),
841845
// A closure parameter clause (`(x: Int, y)`) unwraps to its parameters.
842846
rule!((closureParameterClause parameters: _* @params) => parameter* { params }),

‎unified/extractor/tests/corpus/swift/closures/closure-with-capture-list.output‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,10 @@ top_level source="⟨body⟩"
7070
value:
7171
function_expr source="{ [⟨capture_declaration⟩] in ⟨body⟩ }"
7272
capture_declaration:
73-
variable_declaration source="⟨modifier⟩ ⟨pattern⟩"
73+
variable_declaration source="⟨modifier⟩ ⟨pattern⟩⟨value⟩"
7474
modifier: modifier "weak" source="weak"
7575
pattern: identifier "self" source="self"
76+
value: identifier "self" source="self"
7677
body:
7778
block source="⟨stmt⟩"
7879
stmt:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
private import unified
2+
private import codeql.unified.internal.dataflow.CaptureSsa
3+
import CaptureSsaOutput::ConsistencyChecks

‎unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ module Ast implements AstSig<Location> {
4343
not skipControlFlow(result)
4444
or
4545
n.(FunctionExpr).getCaptureDeclaration(index) = result
46+
or
47+
n.(FunctionDeclaration).getNameNode() = result and index = 0
4648
}
4749

4850
Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() }

‎unified/ql/lib/codeql/unified/internal/FacadeAst.qll‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ module Unified {
5454
this = func.getACaptureDeclaration() and
5555
result = func.getParent()
5656
)
57+
or
58+
exists(FunctionDeclaration func |
59+
// The name of a function declaration is a variable belonging to the enclosing callable.
60+
this = func.getNameNode() and
61+
result = func.getParent()
62+
)
5763
}
5864

5965
/**

‎unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll‎

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -72,35 +72,10 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig<Locatio
7272
}
7373

7474
private AstNode getChild1(AstNode n, int index) {
75-
result = n.(Block).getStmt(index) and
76-
not n instanceof BlockWithGuardStmts
77-
or
7875
result = n.(BlockWithGuardStmts).getTranslatedChild(index)
7976
or
8077
result = n.(LogicalAndRoot).getNthLeaf(index)
8178
or
82-
exists(PatternGuardExpr guard | n = guard |
83-
index = 0 and result = guard.getPattern()
84-
or
85-
index = 1 and result = guard.getValue()
86-
)
87-
or
88-
exists(IfExpr expr | n = expr |
89-
index = 0 and result = expr.getCondition()
90-
or
91-
index = 1 and result = expr.getThen()
92-
or
93-
index = 2 and result = expr.getElse()
94-
)
95-
or
96-
exists(VariableDeclaration decl | n = decl |
97-
index = 0 and result = decl.getPattern()
98-
or
99-
index = 1 and result = decl.getType()
100-
or
101-
index = 2 and result = decl.getValue()
102-
)
103-
or
10479
index = 0 and
10580
relocatedClassMember(n, result)
10681
}
@@ -123,7 +98,7 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig<Locatio
12398
not n instanceof LogicalAndExpr and // also ignore intermediate nodes within a 'logical and' tree
12499
not n instanceof GuardIfStmt and
125100
not relocatedClassMember(_, result) and
126-
index = 0 and
101+
index = result.getParentIndex() and
127102
result = n.getAFieldOrChild()
128103
}
129104

@@ -176,11 +151,6 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig<Locatio
176151

177152
private class LocalVariableDeclarationSiblingShadowingDecl extends SiblingShadowingDecl instanceof LocalVariableDeclaration
178153
{
179-
LocalVariableDeclarationSiblingShadowingDecl() {
180-
// Capture-declarations act as local variables, but are not sibling-shadowing
181-
not this = any(FunctionExpr e).getACaptureDeclaration()
182-
}
183-
184154
override Expr getPattern() { result = LocalVariableDeclaration.super.getPattern() }
185155

186156
override AstNode getRhs() { result = LocalVariableDeclaration.super.getValue() }

‎unified/ql/lib/codeql/unified/internal/dataflow/AllDataFlow.qll‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** Re-exports all the files in the internal dataflow folder (except DataFlowPublic). */
22

33
import CallGraph
4+
import CaptureSsa
45
import Content
56
import ConstructorPatterns
67
import DataFlowCall
@@ -14,3 +15,4 @@ import ParameterPositions
1415
import Step
1516
import TaintTrackingInstantiation
1617
import VariableRefKind
18+
import LocalVariableRefNode
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* SSA for captured variables.
3+
*/
4+
5+
private import unified
6+
private import unified as U
7+
private import AllDataFlow
8+
private import codeql.dataflow.VariableCapture
9+
private import codeql.unified.internal.ControlFlowGraph
10+
private import codeql.unified.internal.ExprPositions
11+
12+
module CaptureSsaInput implements InputSig<Location, BasicBlock> {
13+
final private class FinalCallable = U::Callable;
14+
15+
class Callable extends FinalCallable {
16+
Callable() { this.fromSource() }
17+
18+
predicate isConstructor() { none() } // TODO
19+
}
20+
21+
Callable basicBlockGetEnclosingCallable(BasicBlock bb) { result = bb.getEnclosingCallable() }
22+
23+
class CapturedVariable extends LocalVariable {
24+
CapturedVariable() { this.isCaptured() }
25+
26+
Callable getCallable() { result = super.getDeclaringCallable() }
27+
}
28+
29+
class CapturedParameter extends CapturedVariable {
30+
// This class should be empty, as the capture library never gets to work with a node that corresponds to a `DataFlowInput::ParameterNode`.
31+
//
32+
// When a variable is mentioned in a pattern in the parameter list, the pattern itself (TValueNode) becomes the ParameterNode
33+
// and the assignment to the variable is modeled as an ordinary variable-assignment at the point where the variable mention appears in the CFG.
34+
// Likewise, the receiver is represented by TImplicitParameterNode and its assignment to 'self/this' is modeled as an ordinary assignment.
35+
CapturedParameter() { none() }
36+
}
37+
38+
private class TExpr = TCallableNode or TLocalVariableRefNode;
39+
40+
private predicate isTopLevelCallable(BuilderNode callable) {
41+
callable.isCallable(any(TopLevel t))
42+
}
43+
44+
class Expr extends TExpr {
45+
Expr() { not isTopLevelCallable(this) }
46+
47+
string toString() { result = this.(BuilderNode).toString() }
48+
49+
Location getLocation() { result = this.(BuilderNode).getLocation() }
50+
51+
predicate hasCfgNode(BasicBlock bb, int i) {
52+
this.(LocalVariableRefNode).hasCfgNode(bb, i)
53+
or
54+
exists(DataFlowCallable callable |
55+
this = TCallableNode(callable) and
56+
bb.getNode(i).injects(callable.asSourceCallable())
57+
)
58+
}
59+
}
60+
61+
final private class FinalLocalVariableRefNode = LocalVariableRefNode;
62+
63+
class VariableWrite extends FinalLocalVariableRefNode {
64+
VariableWrite() {
65+
this.getRefKind().isWrite() and super.getVariable() instanceof CapturedVariable
66+
}
67+
68+
CapturedVariable getVariable() { result = super.getVariable() }
69+
}
70+
71+
class VariableRead extends Expr instanceof LocalVariableRefNode {
72+
VariableRead() {
73+
super.getVariable() instanceof CapturedVariable and
74+
super.getRefKind().isRead()
75+
}
76+
77+
CapturedVariable getVariable() { result = super.getVariable() }
78+
}
79+
80+
/**
81+
* Holds if `node` is a possible alias for `callable`.
82+
*/
83+
private predicate callableHasLocalAlias(Callable callable, Stage2Node node) {
84+
node.(BuilderNode).isCallable(callable)
85+
or
86+
exists(Stage2Node prev | callableHasLocalAlias(callable, prev) |
87+
step(prev, any(Step s | s.value()), node)
88+
or
89+
localSsaStep(prev, node, _)
90+
)
91+
or
92+
exists(CapturedVariable var |
93+
callableHasLocalAliasVar(callable, var) and
94+
node.(BuilderNode).isLocalVariableRead(_, var)
95+
)
96+
}
97+
98+
pragma[nomagic]
99+
private predicate callableHasLocalAliasVar(Callable callable, CapturedVariable var) {
100+
exists(BuilderNode ref |
101+
callableHasLocalAlias(callable, ref) and
102+
ref.isLocalVariableWrite(_, var)
103+
)
104+
}
105+
106+
class ClosureExpr extends Expr instanceof TCallableNode {
107+
// TODO: In order to support classes with captures, this needs to cover constructor calls to such classes as well.
108+
private Callable callable;
109+
110+
ClosureExpr() { this.(BuilderNode).isCallable(callable) }
111+
112+
predicate hasBody(Callable body) { callable = body }
113+
114+
predicate hasAliasedAccess(Expr f) {
115+
// TODO: Restrict to places where the function is called or escapes into a call where it might get called.
116+
callableHasLocalAlias(callable, f) and not f.(LocalVariableRefNode).getRefKind().isWrite()
117+
}
118+
}
119+
}
120+
121+
module CaptureSsaOutput = Flow<Location, Cfg, CaptureSsaInput>;
122+
123+
Node getNodeFromCaptureSsaNode(CaptureSsaOutput::ClosureNode n) {
124+
result = TCaptureSsaNode(n)
125+
or
126+
result = n.(CaptureSsaOutput::ExprNode).getExpr()
127+
or
128+
result = n.(CaptureSsaOutput::ExprPostUpdateNode).getExpr().(BuilderNode).getPostUpdateNode()
129+
or
130+
result = n.(CaptureSsaOutput::VariableWriteSourceNode).getVariableWrite()
131+
or
132+
// NOTE: This only supports lambdas at the moment. Local classes in Swift cannot capture variables.
133+
result = n.(CaptureSsaOutput::MallocNode).getClosureExpr()
134+
or
135+
exists(CaptureSsaOutput::ThisParameterNode thisParam, Callable callable |
136+
n = thisParam and
137+
callable = thisParam.getCallable() and
138+
result
139+
.(BuilderNode)
140+
.isImplicitParameter(toDataFlowCallable(callable), any(ParameterPosition p | p.isCallee()))
141+
)
142+
}
143+
144+
CaptureSsaOutput::ClosureNode getCaptureSsaNodeFromNode(Node n) {
145+
n = getNodeFromCaptureSsaNode(result)
146+
}
147+
148+
predicate captureSsaLocalFlowStep(Node node1, Node node2) {
149+
CaptureSsaOutput::localFlowStep(getCaptureSsaNodeFromNode(node1), getCaptureSsaNodeFromNode(node2))
150+
}
151+
152+
predicate captureSsaStoreStep(Node node1, ContentSet contents, Node node2) {
153+
CaptureSsaOutput::storeStep(getCaptureSsaNodeFromNode(node1),
154+
contents.asSingleton().asCapturedVariable(), getCaptureSsaNodeFromNode(node2))
155+
}
156+
157+
predicate captureSsaReadStep(Node node1, ContentSet contents, Node node2) {
158+
CaptureSsaOutput::readStep(getCaptureSsaNodeFromNode(node1),
159+
contents.asSingleton().asCapturedVariable(), getCaptureSsaNodeFromNode(node2))
160+
}
161+
162+
predicate captureSsaClearsContent(Node node, ContentSet contents) {
163+
CaptureSsaOutput::clearsContent(getCaptureSsaNodeFromNode(node),
164+
contents.asSingleton().asCapturedVariable())
165+
}
166+
167+
Node getCaptureSsaPostUpdate(Node pre) {
168+
CaptureSsaOutput::capturePostUpdateNode(getCaptureSsaNodeFromNode(result),
169+
getCaptureSsaNodeFromNode(pre))
170+
}
171+
172+
predicate captureSsaAllowParameterReturnInSelf(Node param) {
173+
exists(Callable callable |
174+
CaptureSsaOutput::heuristicAllowInstanceParameterReturnInSelf(callable) and
175+
param =
176+
getNodeFromCaptureSsaNode(any(CaptureSsaOutput::ThisParameterNode n |
177+
n.getCallable() = callable
178+
))
179+
)
180+
}

‎unified/ql/lib/codeql/unified/internal/dataflow/Content.qll‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ private newtype TContent =
1111
name = [0 .. 20].toString()
1212
or
1313
name = getEnumCaseParameterFieldFromArgument(_, _)
14-
}
14+
} or
15+
TCapturedVariable(CaptureSsaInput::CapturedVariable v)
1516

1617
class Content extends TContent {
1718
string asNamedMember() { this = TNamedMember(result) }
@@ -22,9 +23,13 @@ class Content extends TContent {
2223
result = this.asNamedMember()
2324
or
2425
this.isArrayElement() and result = "ArrayElement"
26+
or
27+
result = this.asCapturedVariable().toString()
2528
}
2629

27-
Location getLocation() { none() }
30+
LocalVariable asCapturedVariable() { this = TCapturedVariable(result) }
31+
32+
Location getLocation() { result = this.asCapturedVariable().getLocation() }
2833
}
2934

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

4752
ContentSet arrayElement() { result.asSingleton().isArrayElement() }
53+
54+
ContentSet capturedVariable(LocalVariable v) { result.asSingleton().asCapturedVariable() = v }
4855
}

0 commit comments

Comments
 (0)