mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
[Concurrency] Introduce runtime detection of data races.
Through various means, it is possible for a synchronous actor-isolated function to escape to another concurrency domain and be called from outside the actor. The problem existed previously, but has become far easier to trigger now that `@escaping` closures and local functions can be actor-isolated. Introduce runtime detection of such data races, where a synchronous actor-isolated function ends up being called from the wrong executor. Do this by emitting an executor check in actor-isolated synchronous functions, where we query the executor in thread-local storage and ensure that it is what we expect. If it isn't, the runtime complains. The runtime's complaints can be controlled with the environment variable `SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL`: 0 - disable checking 1 - warn when a data race is detected 2 - error and abort when a data race is detected At an implementation level, this introduces a new concurrency runtime entry point `_checkExpectedExecutor` that checks the given executor (on which the function should always have been called) against the executor on which is called (which is in thread-local storage). There is a special carve-out here for `@MainActor` code, where we check against the OS's notion of "main thread" as well, so that `@MainActor` code can be called via (e.g.) the Dispatch library's `DispatchQueue.main.async`. The new SIL instruction `extract_executor` performs the lowering of an actor down to its executor, which is implicit in the `hop_to_executor` instruction. Extend the LowerHopToExecutor pass to perform said lowering.
This commit is contained in:
@@ -462,9 +462,6 @@ void SILGenFunction::emitProlog(CaptureInfo captureInfo,
|
||||
}
|
||||
}
|
||||
|
||||
if (!F.isAsync())
|
||||
return;
|
||||
|
||||
// Initialize ExpectedExecutor if the function is an actor-isolated
|
||||
// function or closure.
|
||||
if (auto *funcDecl =
|
||||
@@ -478,9 +475,15 @@ void SILGenFunction::emitProlog(CaptureInfo captureInfo,
|
||||
|
||||
case ActorIsolation::ActorInstance: {
|
||||
assert(selfParam && "no self parameter for ActorInstance isolation");
|
||||
auto loc = RegularLocation::getAutoGeneratedLocation(F.getLocation());
|
||||
ManagedValue selfArg = ManagedValue::forUnmanaged(F.getSelfArgument());
|
||||
ExpectedExecutor = emitLoadActorExecutor(loc, selfArg);
|
||||
// Only produce an executor for actor-isolated functions that are async
|
||||
// or are local functions. The former require a hop, while the latter
|
||||
// are prone to dynamic data races in code that does not enforce Sendable
|
||||
// completely.
|
||||
if (F.isAsync() || funcDecl->isLocalCapture()) {
|
||||
auto loc = RegularLocation::getAutoGeneratedLocation(F.getLocation());
|
||||
ManagedValue selfArg = ManagedValue::forUnmanaged(F.getSelfArgument());
|
||||
ExpectedExecutor = emitLoadActorExecutor(loc, selfArg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -514,14 +517,24 @@ void SILGenFunction::emitProlog(CaptureInfo captureInfo,
|
||||
}
|
||||
|
||||
// Jump to the expected executor.
|
||||
if (ExpectedExecutor)
|
||||
B.createHopToExecutor(
|
||||
if (ExpectedExecutor) {
|
||||
if (F.isAsync()) {
|
||||
// For an async function, hop to the executor.
|
||||
B.createHopToExecutor(
|
||||
RegularLocation::getAutoGeneratedLocation(F.getLocation()),
|
||||
ExpectedExecutor);
|
||||
} else {
|
||||
// For a synchronous function, check that we're on the same executor.
|
||||
// Note: if we "know" that the code is completely Sendable-safe, this
|
||||
// is unnecessary. The type checker will need to make this determination.
|
||||
emitPreconditionCheckExpectedExecutor(
|
||||
RegularLocation::getAutoGeneratedLocation(F.getLocation()),
|
||||
ExpectedExecutor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SILValue SILGenFunction::emitLoadGlobalActorExecutor(Type globalActor) {
|
||||
assert(F.isAsync());
|
||||
CanType actorType = CanType(globalActor);
|
||||
NominalTypeDecl *nominal = actorType->getNominalOrBoundGenericNominal();
|
||||
VarDecl *sharedInstanceDecl = nominal->getGlobalActorInstance();
|
||||
@@ -568,29 +581,7 @@ ExecutorBreadcrumb SILGenFunction::emitHopToTargetActor(SILLocation loc,
|
||||
if (!maybeIso)
|
||||
return ExecutorBreadcrumb();
|
||||
|
||||
auto actorIso = maybeIso.getValue();
|
||||
Optional<SILValue> executor = None;
|
||||
|
||||
switch (actorIso.getKind()) {
|
||||
case ActorIsolation::Unspecified:
|
||||
case ActorIsolation::Independent:
|
||||
break;
|
||||
|
||||
case ActorIsolation::ActorInstance: {
|
||||
// "self" here means the actor instance's "self" value.
|
||||
assert(maybeSelf.hasValue() && "actor-instance but no self provided?");
|
||||
auto self = maybeSelf.getValue();
|
||||
executor = emitLoadActorExecutor(loc, self);
|
||||
break;
|
||||
}
|
||||
|
||||
case ActorIsolation::GlobalActor:
|
||||
case ActorIsolation::GlobalActorUnsafe:
|
||||
executor = emitLoadGlobalActorExecutor(actorIso.getGlobalActor());
|
||||
break;
|
||||
}
|
||||
|
||||
if (executor) {
|
||||
if (auto executor = emitExecutor(loc, *maybeIso, maybeSelf)) {
|
||||
// Record the previous executor to hop back to when we no longer need to
|
||||
// be isolated to the target actor.
|
||||
//
|
||||
@@ -605,6 +596,50 @@ ExecutorBreadcrumb SILGenFunction::emitHopToTargetActor(SILLocation loc,
|
||||
}
|
||||
}
|
||||
|
||||
Optional<SILValue> SILGenFunction::emitExecutor(
|
||||
SILLocation loc, ActorIsolation isolation,
|
||||
Optional<ManagedValue> maybeSelf) {
|
||||
switch (isolation.getKind()) {
|
||||
case ActorIsolation::Unspecified:
|
||||
case ActorIsolation::Independent:
|
||||
return None;
|
||||
|
||||
case ActorIsolation::ActorInstance: {
|
||||
// "self" here means the actor instance's "self" value.
|
||||
assert(maybeSelf.hasValue() && "actor-instance but no self provided?");
|
||||
auto self = maybeSelf.getValue();
|
||||
return emitLoadActorExecutor(loc, self);
|
||||
}
|
||||
|
||||
case ActorIsolation::GlobalActor:
|
||||
case ActorIsolation::GlobalActorUnsafe:
|
||||
return emitLoadGlobalActorExecutor(isolation.getGlobalActor());
|
||||
}
|
||||
}
|
||||
|
||||
void SILGenFunction::emitPreconditionCheckExpectedExecutor(
|
||||
SILLocation loc, SILValue executorOrActor) {
|
||||
auto checkExecutor = SGM.getCheckExpectedExecutor();
|
||||
if (!checkExecutor)
|
||||
return;
|
||||
|
||||
// Get the executor.
|
||||
SILValue executor = B.createExtractExecutor(loc, executorOrActor);
|
||||
|
||||
// Call the library function that performs the checking.
|
||||
auto args = emitSourceLocationArgs(loc.getSourceLoc(), loc);
|
||||
|
||||
emitApplyOfLibraryIntrinsic(loc, checkExecutor, SubstitutionMap(),
|
||||
{
|
||||
args.filenameStartPointer,
|
||||
args.filenameLength,
|
||||
args.filenameIsAscii,
|
||||
args.line,
|
||||
ManagedValue::forUnmanaged(executor)
|
||||
},
|
||||
SGFContext());
|
||||
}
|
||||
|
||||
void ExecutorBreadcrumb::emit(SILGenFunction &SGF, SILLocation loc) {
|
||||
if (Executor)
|
||||
SGF.B.createHopToExecutor(loc, Executor);
|
||||
|
||||
Reference in New Issue
Block a user