Skip to content

Commit d593623

Browse files
committed
Do not insert dynamic exclusivity checks in closure cycles
Closure cycles were originally enforced "conservatively". Real code does, however, use recursive local functions. And it's surprisingly easy to create false exclusivity violations in those cases. This is safe as long as we can rely on SILGen to keep captured variables in boxes if the capture escapes in any closure that may call other closures that capture the same variable. Fixes #61404 Dynamic exclusivity checking bug with nested functions. #61404 Fixes rdar://102056143 (assertion failure due to exclusivity checker - AccessEnforcementSelection should handle recursive local captures)
1 parent b206c76 commit d593623

File tree

3 files changed

+84
-8
lines changed

3 files changed

+84
-8
lines changed

lib/SILOptimizer/Mandatory/AccessEnforcementSelection.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ raw_ostream &operator<<(raw_ostream &os, const AddressCapture &capture) {
9999

100100
// For each non-escaping closure, record the indices of arguments that
101101
// require dynamic enforcement.
102+
//
103+
// A note on closure cycles: local functions can be recursive, creating closure
104+
// cycles. DynamicCaptures ignores such cycles, simply processing the call graph
105+
// top-down. This relies on a simple rule: if a captured variable is passed as a
106+
// box a local function (presumably because the function escapes), then it must
107+
// also be passed as a box to any other local function called by the
108+
// first. Therefore, if any capture escapes in a closure cycle, then it must be
109+
// passed as a box in all closures within the cycle. DynamicCaptures does not
110+
// care about boxes, because they are always dynamically enforced.
102111
class DynamicCaptures {
103112
const ClosureFunctionOrder &closureOrder;
104113

@@ -127,10 +136,10 @@ class DynamicCaptures {
127136
}
128137

129138
bool isDynamic(SILFunctionArgument *arg) const {
130-
// If the current function is a local function that directly or indirectly
131-
// refers to itself, then conservatively assume dynamic enforcement.
132-
if (closureOrder.isHeadOfClosureCycle(arg->getFunction()))
133-
return true;
139+
// This closure may be the head of a closure cycle. That's ok, because we
140+
// only care about whether this argument escapes in the calling function
141+
// this is *not* part of the cycle. If the capture escapes anywhere in the
142+
// cycle, then it is passed as a box to all closures in that cycle.
134143

135144
auto pos = dynamicCaptureMap.find(arg->getFunction());
136145
if (pos == dynamicCaptureMap.end())

test/Interpreter/enforce_exclusive_access.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,56 @@ ExclusiveAccessTestSuite.test("directlyAppliedEscapingConflict") {
383383
}()
384384
}
385385

386+
// rdar://102056143 - recursive locals inside a mutating method
387+
// effectively capture self as inout even if they don't modify
388+
// self. This should not trigger a runtime trap.
389+
struct RecursiveLocalDuringMutation {
390+
var flag: Bool = false
391+
392+
mutating func update() {
393+
func local1(_ done: Bool) -> Bool {
394+
if !done {
395+
return local2()
396+
}
397+
return flag
398+
}
399+
func local2() -> Bool {
400+
return local1(true)
401+
}
402+
flag = !local1(false)
403+
}
404+
}
405+
406+
ExclusiveAccessTestSuite.test("recursiveLocalDuringMutation") {
407+
var v = RecursiveLocalDuringMutation()
408+
v.update()
409+
_blackHole(v.flag)
410+
}
411+
412+
// rdar://102056143 - negative test. Make sure we still catch true
413+
// violations on recursive captures.
414+
struct RecursiveCaptureViolation {
415+
func testBadness() -> Bool {
416+
var flag = false
417+
func local1(_ x: inout Bool) {
418+
if !flag {
419+
x = true
420+
local2()
421+
}
422+
}
423+
func local2() {
424+
local1(&flag)
425+
}
426+
local2()
427+
return flag
428+
}
429+
}
430+
431+
ExclusiveAccessTestSuite.test("recursiveCaptureViolation") {
432+
var s = RecursiveCaptureViolation()
433+
expectCrashLater()
434+
s.testBadness()
435+
_blackHole(s)
436+
}
437+
386438
runAllTests()

test/SILOptimizer/access_enforcement_selection.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,16 @@ public func recaptureStack() -> Int {
101101

102102

103103
// -----------------------------------------------------------------------------
104-
// Inout access in a recursive closure currently has dynamic enforcement
105-
// because enforcement selection cannot visit all closure scopes before
106-
// selecting enforcement within the closure.
104+
// Inout access in a recursive non-escaping closure currently has
105+
// static enforcement.
107106

108107
public protocol Apply {
109108
func apply(_ body: (Apply) -> ())
110109
}
111110

112111
// CHECK-LABEL: sil private @$s28access_enforcement_selection20testRecursiveClosure1a1xyAA5Apply_p_SiztF9localFuncL_1byAaE_p_tF : $@convention(thin) (@in_guaranteed any Apply, @inout_aliasable Int) -> () {
113112
// CHECK: bb0(%0 : $*any Apply, %1 : @closureCapture $*Int):
114-
// CHECK: begin_access [modify] [dynamic] %1 : $*Int
113+
// CHECK: begin_access [modify] [static] %1 : $*Int
115114
// CHECK-LABEL: } // end sil function '$s28access_enforcement_selection20testRecursiveClosure1a1xyAA5Apply_p_SiztF9localFuncL_1byAaE_p_tF'
116115
public func testRecursiveClosure(a: Apply, x: inout Int) {
117116
func localFunc(b: Apply) {
@@ -123,3 +122,19 @@ public func testRecursiveClosure(a: Apply, x: inout Int) {
123122
}
124123
a.apply(localFunc)
125124
}
125+
126+
// CHECK-LABEL: sil private @$s28access_enforcement_selection25testRecursiveLocalCapture1iys5Int64Vz_tF6local2L_yyF : $@convention(thin) (@inout_aliasable Int64) -> () {
127+
// CHECK: begin_access [modify] [static] %0 : $*Int64
128+
// CHECK-LABEL: } // end sil function '$s28access_enforcement_selection25testRecursiveLocalCapture1iys5Int64Vz_tF6local2L_yyF'
129+
func testRecursiveLocalCapture(i: inout Int64) {
130+
func local1() {
131+
if i < 1 {
132+
local2()
133+
}
134+
}
135+
func local2() {
136+
i = i + 1
137+
local1()
138+
}
139+
local1()
140+
}

0 commit comments

Comments
 (0)