Skip to content

Commit c51fbb9

Browse files
committed
[Exclusivity] Statically enforce exclusive access in noescape closures
Use the AccessSummaryAnalysis to statically enforce exclusive access for noescape closures passed as arguments to functions. We will now diagnose when a function is passed a noescape closure that begins an access on capture when that same capture already has a conflicting access in progress at the time the function is applied. The interprocedural analysis is not yet stored-property sensitive (unlike the intraprocedural analysis), so this will report violations on accesses to separate stored properties of the same struct. rdar://problem/32020710
1 parent 06b9ed7 commit c51fbb9

File tree

5 files changed

+391
-17
lines changed

5 files changed

+391
-17
lines changed

lib/SILOptimizer/Analysis/AccessSummaryAnalysis.cpp

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ void AccessSummaryAnalysis::processArgument(FunctionInfo *info,
104104
break;
105105
case ValueKind::CopyAddrInst:
106106
case ValueKind::ExistentialMetatypeInst:
107+
case ValueKind::ValueMetatypeInst:
107108
case ValueKind::LoadInst:
108109
case ValueKind::OpenExistentialAddrInst:
109110
case ValueKind::ProjectBlockStorageInst:
@@ -114,11 +115,41 @@ void AccessSummaryAnalysis::processArgument(FunctionInfo *info,
114115
break;
115116
default:
116117
// TODO: These requirements should be checked for in the SIL verifier.
117-
llvm_unreachable("Unrecognized argument use");
118+
// This is an assertion rather than llvm_unreachable() because
119+
// it is likely the whitelist above for scenarios in which we'ren
120+
// not generating access markers is not comprehensive.
121+
assert(false && "Unrecognized argument use");
122+
break;
118123
}
119124
}
120125
}
121126

127+
#ifndef NDEBUG
128+
/// Sanity check to make sure that a noescape partial apply is
129+
/// only ultimately used by an apply, a try_apply or as an argument (but not
130+
/// the called function) in a partial_apply.
131+
/// TODO: This really should be checked in the SILVerifier.
132+
static bool isExpectedUseOfNoEscapePartialApply(SILInstruction *user) {
133+
if (!user)
134+
return true;
135+
136+
// It is fine to call the partial apply
137+
if (isa<ApplyInst>(user) || isa<TryApplyInst>(user)) {
138+
return true;
139+
}
140+
141+
if (isa<ConvertFunctionInst>(user)) {
142+
return isExpectedUseOfNoEscapePartialApply(user->getSingleUse()->getUser());
143+
}
144+
145+
if (auto *PAI = dyn_cast<PartialApplyInst>(user)) {
146+
return user != PAI->getCallee();
147+
}
148+
149+
return false;
150+
}
151+
#endif
152+
122153
void AccessSummaryAnalysis::processPartialApply(FunctionInfo *callerInfo,
123154
unsigned callerArgumentIndex,
124155
PartialApplyInst *apply,
@@ -133,12 +164,9 @@ void AccessSummaryAnalysis::processPartialApply(FunctionInfo *callerInfo,
133164
assert(isa<FunctionRefInst>(apply->getCallee()) &&
134165
"Noescape partial apply of non-functionref?");
135166

136-
// Make sure the partial_apply is used by an apply and not another
137-
// partial_apply
138167
SILInstruction *user = apply->getSingleUse()->getUser();
139-
assert((isa<ApplyInst>(user) || isa<TryApplyInst>(user) ||
140-
isa<ConvertFunctionInst>(user)) &&
141-
"noescape partial_apply has non-apply use!");
168+
assert(isExpectedUseOfNoEscapePartialApply(user) &&
169+
"noescape partial_apply has unexpected use!");
142170
(void)user;
143171

144172
// The arguments to partial_apply are a suffix of the arguments to the

lib/SILOptimizer/Mandatory/DiagnoseStaticExclusivity.cpp

Lines changed: 167 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,26 +163,75 @@ class AccessedStorage {
163163
}
164164
};
165165

166+
enum class RecordedAccessKind {
167+
/// The access was for a 'begin_access' instruction in the current function
168+
/// being checked.
169+
BeginInstruction,
170+
171+
/// The access was inside noescape closure that we either
172+
/// passed to function or called directly. It results from applying the
173+
/// the summary of the closure to the closure's captures.
174+
NoescapeClosureCapture
175+
};
176+
166177
/// Records an access to an address and the single subpath of projections
167178
/// that was performed on the address, if such a single subpath exists.
168179
class RecordedAccess {
169180
private:
170-
BeginAccessInst *Inst;
171-
const IndexTrieNode *SubPath;
181+
RecordedAccessKind RecordKind;
182+
union {
183+
BeginAccessInst *Inst;
184+
struct {
185+
SILAccessKind ClosureAccessKind;
186+
SILLocation ClosureAccessLoc;
187+
};
188+
};
172189

190+
const IndexTrieNode *SubPath;
173191
public:
174-
RecordedAccess(BeginAccessInst *BAI, const IndexTrieNode *SubPath)
175-
: Inst(BAI), SubPath(SubPath) {}
192+
RecordedAccess(BeginAccessInst *BAI, const IndexTrieNode *SubPath) :
193+
RecordKind(RecordedAccessKind::BeginInstruction), Inst(BAI),
194+
SubPath(SubPath) { }
195+
196+
RecordedAccess(SILAccessKind ClosureAccessKind,
197+
SILLocation ClosureAccessLoc, const IndexTrieNode *SubPath) :
198+
RecordKind(RecordedAccessKind::NoescapeClosureCapture),
199+
ClosureAccessKind(ClosureAccessKind), ClosureAccessLoc(ClosureAccessLoc),
200+
SubPath(SubPath) { }
201+
202+
RecordedAccessKind getRecordKind() const {
203+
return RecordKind;
204+
}
176205

177-
BeginAccessInst *getInstruction() const { return Inst; }
206+
BeginAccessInst *getInstruction() const {
207+
assert(RecordKind == RecordedAccessKind::BeginInstruction);
208+
return Inst;
209+
}
178210

179-
SILAccessKind getAccessKind() const { return Inst->getAccessKind(); }
211+
SILAccessKind getAccessKind() const {
212+
switch (RecordKind) {
213+
case RecordedAccessKind::BeginInstruction:
214+
return Inst->getAccessKind();
215+
case RecordedAccessKind::NoescapeClosureCapture:
216+
return ClosureAccessKind;
217+
};
218+
}
180219

181-
SILLocation getAccessLoc() const { return Inst->getLoc(); }
220+
SILLocation getAccessLoc() const {
221+
switch (RecordKind) {
222+
case RecordedAccessKind::BeginInstruction:
223+
return Inst->getLoc();
224+
case RecordedAccessKind::NoescapeClosureCapture:
225+
return ClosureAccessLoc;
226+
};
227+
}
182228

183-
const IndexTrieNode *getSubPath() const { return SubPath; }
229+
const IndexTrieNode *getSubPath() const {
230+
return SubPath;
231+
}
184232
};
185233

234+
186235
/// Records the in-progress accesses to a given sub path.
187236
class SubAccessInfo {
188237
public:
@@ -676,9 +725,11 @@ static void diagnoseExclusivityViolation(const ConflictingAccess &Violation,
676725
diagnose(Ctx, MainAccess.getAccessLoc().getSourceLoc(), DiagnosticID,
677726
PathDescription, AccessKindForMain);
678727
D.highlight(RangeForMain);
679-
tryFixItWithCallToCollectionSwapAt(FirstAccess.getInstruction(),
680-
SecondAccess.getInstruction(),
681-
CallsToSwap, Ctx, D);
728+
if (SecondAccess.getRecordKind() == RecordedAccessKind::BeginInstruction) {
729+
tryFixItWithCallToCollectionSwapAt(FirstAccess.getInstruction(),
730+
SecondAccess.getInstruction(),
731+
CallsToSwap, Ctx, D);
732+
}
682733
} else {
683734
auto DiagnosticID = (Ctx.LangOpts.isSwiftVersion3() ?
684735
diag::exclusivity_access_required_unknown_decl_swift3 :
@@ -868,6 +919,102 @@ Optional<RecordedAccess> shouldReportAccess(const AccessInfo &Info,
868919
return Info.conflictsWithAccess(Kind, SubPath);
869920
}
870921

922+
/// Use the summary analysis to check whether a call to the given
923+
/// function would conflict with any in progress accesses. The starting
924+
/// index indicates what index into the the callee's parameters the
925+
/// arguments array starts at -- this is useful for partial_apply functions,
926+
/// which pass only a suffix of the callee's arguments at the apply site.
927+
static void checkForViolationWithCall(
928+
const StorageMap &Accesses, SILFunction *Callee, unsigned StartingAtIndex,
929+
OperandValueArrayRef Arguments, AccessSummaryAnalysis *ASA,
930+
llvm::SmallVectorImpl<ConflictingAccess> &ConflictingAccesses) {
931+
const AccessSummaryAnalysis::FunctionSummary &FS =
932+
ASA->getOrCreateSummary(Callee);
933+
934+
// For each argument in the suffix of the callee arguments being passed
935+
// at this call site, determine whether the arguments will be accessed
936+
// in a way that conflicts with any currently in progress accesses.
937+
// If so, diagnose.
938+
for (unsigned ArgumentIndex : indices(Arguments)) {
939+
unsigned CalleeIndex = StartingAtIndex + ArgumentIndex;
940+
941+
const AccessSummaryAnalysis::ArgumentSummary &AS =
942+
FS.getAccessForArgument(CalleeIndex);
943+
Optional<SILAccessKind> Kind = AS.getAccessKind();
944+
if (!Kind)
945+
continue;
946+
947+
SILValue Argument = Arguments[ArgumentIndex];
948+
assert(Argument->getType().isAddress());
949+
950+
const AccessedStorage &Storage = findAccessedStorage(Argument);
951+
auto AccessIt = Accesses.find(Storage);
952+
if (AccessIt == Accesses.end())
953+
continue;
954+
const AccessInfo &Info = AccessIt->getSecond();
955+
956+
// TODO: For now, treat a summarized access as an access to the whole
957+
// address. Once the summary analysis is sensitive to stored properties,
958+
// this should be updated look at the subpaths from the summary.
959+
const IndexTrieNode *SubPath = ASA->getSubPathTrieRoot();
960+
if (auto Conflict = shouldReportAccess(Info, *Kind, SubPath)) {
961+
SILLocation AccessLoc = AS.getAccessLoc();
962+
const auto &SecondAccess = RecordedAccess(*Kind, AccessLoc, SubPath);
963+
ConflictingAccesses.emplace_back(Storage, *Conflict, SecondAccess);
964+
}
965+
}
966+
}
967+
968+
/// Look through a value passed as a function argument to determine whether
969+
/// it is a partial_apply.
970+
static PartialApplyInst *lookThroughForPartialApply(SILValue V) {
971+
while (true) {
972+
if (auto CFI = dyn_cast<ConvertFunctionInst>(V)) {
973+
V = CFI->getOperand();
974+
continue;
975+
}
976+
977+
if (auto *PAI = dyn_cast<PartialApplyInst>(V))
978+
return PAI;
979+
980+
return nullptr;
981+
}
982+
}
983+
984+
/// Checks whether any of the arguments to the apply are closures and diagnoses
985+
/// if any of the @inout_aliasable captures passed to those closures have
986+
/// in-progress accesses that would conflict with any access the summary
987+
/// says the closure would perform.
988+
static void checkForViolationsInNoEscapeClosures(
989+
const StorageMap &Accesses, FullApplySite FAS, AccessSummaryAnalysis *ASA,
990+
llvm::SmallVectorImpl<ConflictingAccess> &ConflictingAccesses) {
991+
992+
SILFunction *Callee = FAS.getCalleeFunction();
993+
if (Callee && !Callee->empty()) {
994+
// Check for violation with directly called closure
995+
checkForViolationWithCall(Accesses, Callee, 0, FAS.getArguments(), ASA,
996+
ConflictingAccesses);
997+
}
998+
999+
// Check for violation with closures passed as arguments
1000+
for (SILValue Argument : FAS.getArguments()) {
1001+
auto *PAI = lookThroughForPartialApply(Argument);
1002+
if (!PAI)
1003+
continue;
1004+
1005+
SILFunction *Closure = PAI->getCalleeFunction();
1006+
if (!Closure || Closure->empty())
1007+
continue;
1008+
1009+
// Check the closure's captures, which are a suffix of the closure's
1010+
// parameters.
1011+
unsigned StartIndex =
1012+
Closure->getArguments().size() - PAI->getNumCallArguments();
1013+
checkForViolationWithCall(Accesses, Closure, StartIndex,
1014+
PAI->getArguments(), ASA, ConflictingAccesses);
1015+
}
1016+
}
1017+
8711018
static void checkStaticExclusivity(SILFunction &Fn, PostOrderFunctionInfo *PO,
8721019
AccessSummaryAnalysis *ASA) {
8731020
// The implementation relies on the following SIL invariants:
@@ -966,8 +1113,17 @@ static void checkStaticExclusivity(SILFunction &Fn, PostOrderFunctionInfo *PO,
9661113
// Record calls to swap() for potential Fix-Its.
9671114
if (isCallToStandardLibrarySwap(AI, Fn.getASTContext()))
9681115
CallsToSwap.push_back(AI);
1116+
else
1117+
checkForViolationsInNoEscapeClosures(Accesses, AI, ASA,
1118+
ConflictingAccesses);
1119+
continue;
9691120
}
9701121

1122+
if (auto *TAI = dyn_cast<TryApplyInst>(&I)) {
1123+
checkForViolationsInNoEscapeClosures(Accesses, TAI, ASA,
1124+
ConflictingAccesses);
1125+
continue;
1126+
}
9711127
// Sanity check to make sure entries are properly removed.
9721128
assert((!isa<ReturnInst>(&I) || Accesses.size() == 0) &&
9731129
"Entries were not properly removed?!");

test/SILOptimizer/access_summary_analysis.sil

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,34 @@ bb0(%0 : $*Int, %1 : $*Int, %2 : $*Int):
200200
return %8 : $()
201201
}
202202

203+
204+
sil @takesAutoClosureReturningGeneric : $@convention(thin) <T where T : Equatable> (@owned @callee_owned () -> (@out T, @error Error)) -> ()
205+
sil @thunkForAutoClosure : $@convention(thin) (@owned @callee_owned () -> (Int, @error Error)) -> (@out Int, @error Error)
206+
207+
// CHECK-LABEL: @readsAndThrows
208+
// CHECK-NEXT: (read)
209+
sil private @readsAndThrows : $@convention(thin) (@inout_aliasable Int) -> (Int, @error Error) {
210+
bb0(%0 : $*Int):
211+
%3 = begin_access [read] [unknown] %0 : $*Int
212+
%4 = load %3 : $*Int
213+
end_access %3 : $*Int
214+
return %4 : $Int
215+
}
216+
217+
// CHECK-LABEL: @passPartialApplyAsArgumentToPartialApply
218+
// CHECK-NEXT: (read)
219+
sil hidden @passPartialApplyAsArgumentToPartialApply : $@convention(thin) (@inout_aliasable Int) -> () {
220+
bb0(%0 : $*Int):
221+
%2 = function_ref @takesAutoClosureReturningGeneric : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@owned @callee_owned () -> (@out τ_0_0, @error Error)) -> ()
222+
%3 = function_ref @readsAndThrows : $@convention(thin) (@inout_aliasable Int) -> (Int, @error Error)
223+
%4 = partial_apply %3(%0) : $@convention(thin) (@inout_aliasable Int) -> (Int, @error Error)
224+
%5 = function_ref @thunkForAutoClosure : $@convention(thin) (@owned @callee_owned () -> (Int, @error Error)) -> (@out Int, @error Error)
225+
%6 = partial_apply %5(%4) : $@convention(thin) (@owned @callee_owned () -> (Int, @error Error)) -> (@out Int, @error Error)
226+
%7 = apply %2<Int>(%6) : $@convention(thin) <τ_0_0 where τ_0_0 : Equatable> (@owned @callee_owned () -> (@out τ_0_0, @error Error)) -> ()
227+
%8 = tuple ()
228+
return %8 : $()
229+
}
230+
203231
// CHECK-LABEL: @selfRecursion
204232
// CHECK-NEXT: (modify, none)
205233
sil private @selfRecursion : $@convention(thin) (@inout_aliasable Int, Int) -> () {

0 commit comments

Comments
 (0)