Skip to content

Commit 86f5441

Browse files
authored
[SerialExecutor] SerialExecutor.checkIsolated() to check its own tracking for isolation checks (#71172)
1 parent 233c167 commit 86f5441

25 files changed

+831
-47
lines changed

include/swift/ABI/Executor.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,12 @@ class SerialExecutorRef {
155155
return reinterpret_cast<DefaultActor*>(Identity);
156156
}
157157

158+
bool hasSerialExecutorWitnessTable() const {
159+
return !isGeneric() && !isDefaultActor();
160+
}
161+
158162
const SerialExecutorWitnessTable *getSerialExecutorWitnessTable() const {
159-
assert(!isGeneric() && !isDefaultActor());
163+
assert(hasSerialExecutorWitnessTable());
160164
auto table = Implementation & WitnessTableMask;
161165
return reinterpret_cast<const SerialExecutorWitnessTable*>(table);
162166
}

include/swift/Runtime/Bincompat.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -47,6 +47,38 @@ bool useLegacySwiftValueUnboxingInCasting();
4747
/// if present.
4848
bool useLegacySwiftObjCHashing();
4949

50+
/// Legacy semantics allowed for the `swift_task_reportUnexpectedExecutor` to
51+
/// only log a warning. This changes in future releases and this function
52+
/// will fatal error always.
53+
///
54+
/// Similarly, the internal runtime function
55+
/// `swift_task_isCurrentExecutor(expected)` was previously allowed to return
56+
/// `false`. In future releases it will call into `checkIsolated`, and CRASH
57+
/// when previously it would have returned false.
58+
///
59+
/// Because some applications were running with "isolation warnings" and
60+
/// those call into the `isCurrentExecutor` API and expected warnings to be
61+
/// logged, but they ignored those warnings we cannot make them crashing,
62+
/// and must check if the app was built against a new.
63+
///
64+
/// Old behavior:
65+
/// - `swift_task_isCurrentExecutorImpl` cannot crash and does NOT invoke
66+
/// `SerialExecutor.checkIsolated`
67+
/// - `swift_task_isCurrentExecutorImpl` does not invoke `checkIsolated`
68+
/// - logging a warning on concurrency violation is allowed
69+
///
70+
/// New behavior:
71+
/// - always fatal error in `swift_task_reportUnexpectedExecutor`
72+
/// - `swift_task_isCurrentExecutorImpl` will crash when it would have returned
73+
/// false
74+
/// - `swift_task_isCurrentExecutorImpl` does invoke `checkIsolated` when other
75+
/// checks failed
76+
///
77+
/// This can be overridden by using `SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL=1`
78+
/// or `SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE=crash|nocrash`
79+
SWIFT_RUNTIME_STDLIB_SPI
80+
bool swift_bincompat_useLegacyNonCrashingExecutorChecks();
81+
5082
} // namespace bincompat
5183

5284
} // namespace runtime

include/swift/Runtime/Concurrency.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,11 @@ void swift_task_enqueue(Job *job, SerialExecutorRef executor);
715715
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
716716
void swift_task_enqueueGlobal(Job *job);
717717

718+
/// Invoke an executor's `checkIsolated` or otherwise equivalent API,
719+
/// that will crash if the current executor is NOT the passed executor.
720+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
721+
void swift_task_checkIsolated(SerialExecutorRef executor);
722+
718723
/// A count in nanoseconds.
719724
using JobDelay = unsigned long long;
720725

@@ -729,6 +734,9 @@ void swift_task_enqueueGlobalWithDeadline(long long sec, long long nsec,
729734
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
730735
void swift_task_enqueueMainExecutor(Job *job);
731736

737+
/// WARNING: This method is expected to CRASH when caller is not on the
738+
/// expected executor.
739+
///
732740
/// Return true if the caller is running in a Task on the passed Executor.
733741
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
734742
bool swift_task_isOnExecutor(
@@ -781,6 +789,12 @@ SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_hook)(
781789
int clock, Job *job,
782790
swift_task_enqueueGlobalWithDeadline_original original);
783791

792+
typedef SWIFT_CC(swift) void (*swift_task_checkIsolated_original)(SerialExecutorRef executor);
793+
SWIFT_EXPORT_FROM(swift_Concurrency)
794+
SWIFT_CC(swift) void (*swift_task_checkIsolated_hook)(
795+
SerialExecutorRef executor, swift_task_checkIsolated_original original);
796+
797+
784798
typedef SWIFT_CC(swift) bool (*swift_task_isOnExecutor_original)(
785799
HeapObject *executor,
786800
const Metadata *selfType,

stdlib/public/Concurrency/Actor.cpp

Lines changed: 160 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
#include "../CompatibilityOverride/CompatibilityOverride.h"
2323
#include "swift/ABI/Actor.h"
2424
#include "swift/ABI/Task.h"
25+
#include "TaskPrivate.h"
2526
#include "swift/Basic/ListMerger.h"
2627
#include "swift/Concurrency/Actor.h"
2728
#include "swift/Runtime/AccessibleFunction.h"
2829
#include "swift/Runtime/Atomic.h"
30+
#include "swift/Runtime/Bincompat.h"
2931
#include "swift/Runtime/Casting.h"
3032
#include "swift/Runtime/DispatchShims.h"
3133
#include "swift/Threading/Mutex.h"
@@ -313,47 +315,193 @@ bool _task_serialExecutor_isSameExclusiveExecutionContext(
313315
const Metadata *selfType,
314316
const SerialExecutorWitnessTable *wtable);
315317

318+
// We currently still support "legacy mode" in which isCurrentExecutor is NOT
319+
// allowed to crash, because it is used to power "log warnings" data race
320+
// detector. This mode is going away in Swift 6, but until then we allow this.
321+
// This override exists primarily to be able to test both code-paths.
322+
enum IsCurrentExecutorCheckMode: unsigned {
323+
/// The default mode when an app was compiled against "new" enough SDK.
324+
/// It allows crashing in isCurrentExecutor, and calls into `checkIsolated`.
325+
Default_UseCheckIsolated_AllowCrash,
326+
/// Legacy mode; Primarily to support old applications which used data race
327+
/// detector with "warning" mode, which is no longer supported. When such app
328+
/// is re-compiled against a new SDK, it will see crashes in what was
329+
/// previously warnings; however, until until recompiled, warnings will be
330+
/// used, and `checkIsolated` cannot be invoked.
331+
Legacy_NoCheckIsolated_NonCrashing,
332+
};
333+
static IsCurrentExecutorCheckMode isCurrentExecutorMode =
334+
Default_UseCheckIsolated_AllowCrash;
335+
336+
337+
// Shimming call to Swift runtime because Swift Embedded does not have
338+
// these symbols defined
339+
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
340+
#if !SWIFT_CONCURRENCY_EMBEDDED
341+
swift::runtime::bincompat::
342+
swift_bincompat_useLegacyNonCrashingExecutorChecks();
343+
#endif
344+
return false;
345+
}
346+
347+
// Check override of executor checking mode.
348+
static void checkIsCurrentExecutorMode(void *context) {
349+
auto useLegacyMode =
350+
swift_bincompat_useLegacyNonCrashingExecutorChecks();
351+
352+
// Potentially, override the platform detected mode, primarily used in tests.
353+
#if SWIFT_STDLIB_HAS_ENVIRON
354+
const char *modeStr = getenv("SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE");
355+
if (!modeStr)
356+
return;
357+
358+
if (strcmp(modeStr, "nocrash") == 0) {
359+
useLegacyMode = Legacy_NoCheckIsolated_NonCrashing;
360+
} else if (strcmp(modeStr, "crash") == 0) {
361+
useLegacyMode = Default_UseCheckIsolated_AllowCrash;
362+
} // else, just use the platform detected mode
363+
#endif // SWIFT_STDLIB_HAS_ENVIRON
364+
isCurrentExecutorMode = useLegacyMode ? Legacy_NoCheckIsolated_NonCrashing
365+
: Default_UseCheckIsolated_AllowCrash;
366+
}
367+
316368
SWIFT_CC(swift)
317-
static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef executor) {
369+
static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor) {
318370
auto current = ExecutorTrackingInfo::current();
319371

372+
// To support old applications on apple platforms which assumed this call
373+
// does not crash, try to use a more compatible mode for those apps.
374+
static swift::once_t checkModeToken;
375+
swift::once(checkModeToken, checkIsCurrentExecutorMode, nullptr);
376+
320377
if (!current) {
321-
// TODO(ktoso): checking the "is main thread" is not correct, main executor can be not main thread, relates to rdar://106188692
322-
return executor.isMainExecutor() && isExecutingOnMainThread();
378+
// We have no current executor, i.e. we are running "outside" of Swift
379+
// Concurrency. We could still be running on a thread/queue owned by
380+
// the expected executor however, so we need to try a bit harder before
381+
// we fail.
382+
383+
// Are we expecting the main executor and are using the main thread?
384+
if (expectedExecutor.isMainExecutor() && isExecutingOnMainThread()) {
385+
// Due to compatibility with pre-checkIsolated code, we cannot remove
386+
// this special handling. CheckIsolated can handle this if the expected
387+
// executor is the main queue / main executor, however, if we cannot call
388+
// checkIsolated we cannot rely on it to handle this.
389+
// TODO: consider removing this branch when `useCrashingCheckIsolated=true`
390+
return true;
391+
}
392+
393+
// Otherwise, as last resort, let the expected executor check using
394+
// external means, as it may "know" this thread is managed by it etc.
395+
if (isCurrentExecutorMode == Default_UseCheckIsolated_AllowCrash) {
396+
swift_task_checkIsolated(expectedExecutor);
397+
// checkIsolated did not crash, so we are on the right executor, after all!
398+
return true;
399+
}
400+
401+
assert(isCurrentExecutorMode == Legacy_NoCheckIsolated_NonCrashing);
402+
return false;
323403
}
324404

325-
auto currentExecutor = current->getActiveExecutor();
326-
if (currentExecutor == executor) {
405+
SerialExecutorRef currentExecutor = current->getActiveExecutor();
406+
407+
// Fast-path: the executor is exactly the same memory address;
408+
// We assume executors do not come-and-go appearing under the same address,
409+
// and treat pointer equality of executors as good enough to assume the executor.
410+
if (currentExecutor == expectedExecutor) {
411+
return true;
412+
}
413+
414+
// Fast-path, specialize the common case of comparing two main executors.
415+
if (currentExecutor.isMainExecutor() && expectedExecutor.isMainExecutor()) {
327416
return true;
328417
}
329418

330-
if (executor.isComplexEquality()) {
419+
// If the expected executor is "default" then we should have matched
420+
// by pointer equality already with the current executor.
421+
if (expectedExecutor.isDefaultActor()) {
422+
// If the expected executor is a default actor, it makes no sense to try
423+
// the 'checkIsolated' call, it must be equal to the other actor, or it is
424+
// not the same isolation domain.
425+
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption");
426+
return false;
427+
}
428+
429+
if (expectedExecutor.isMainExecutor() && !currentExecutor.isMainExecutor()) {
430+
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
431+
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected MainActor executor");
432+
return false;
433+
} else if (!expectedExecutor.isMainExecutor() && currentExecutor.isMainExecutor()) {
434+
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
435+
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected not-MainActor executor");
436+
return false;
437+
}
438+
439+
if (expectedExecutor.isComplexEquality()) {
331440
if (!swift_compareWitnessTables(
332441
reinterpret_cast<const WitnessTable*>(currentExecutor.getSerialExecutorWitnessTable()),
333-
reinterpret_cast<const WitnessTable*>(executor.getSerialExecutorWitnessTable()))) {
442+
reinterpret_cast<const WitnessTable*>(expectedExecutor.getSerialExecutorWitnessTable()))) {
334443
// different witness table, we cannot invoke complex equality call
335444
return false;
336445
}
446+
337447
// Avoid passing nulls to Swift for the isSame check:
338-
if (!currentExecutor.getIdentity() || !executor.getIdentity()) {
448+
if (!currentExecutor.getIdentity() || !expectedExecutor.getIdentity()) {
339449
return false;
340450
}
341451

342-
return _task_serialExecutor_isSameExclusiveExecutionContext(
452+
auto result = _task_serialExecutor_isSameExclusiveExecutionContext(
343453
currentExecutor.getIdentity(),
344-
executor.getIdentity(),
454+
expectedExecutor.getIdentity(),
345455
swift_getObjectType(currentExecutor.getIdentity()),
346-
executor.getSerialExecutorWitnessTable());
456+
expectedExecutor.getSerialExecutorWitnessTable());
457+
458+
return result;
459+
}
460+
461+
// This provides a last-resort check by giving the expected SerialExecutor the
462+
// chance to perform a check using some external knowledge if perhaps we are,
463+
// after all, on this executor, but the Swift concurrency runtime was just not
464+
// aware.
465+
//
466+
// Unless handled in `swift_task_checkIsolated` directly, this should call
467+
// through to the executor's `SerialExecutor.checkIsolated`.
468+
//
469+
// This call is expected to CRASH, unless it has some way of proving that
470+
// we're actually indeed running on this executor.
471+
//
472+
// For example, when running outside of Swift concurrency tasks, but trying to
473+
// `MainActor.assumeIsolated` while executing DIRECTLY on the main dispatch
474+
// queue, this allows Dispatch to check for this using its own tracking
475+
// mechanism, and thus allow the assumeIsolated to work correctly, even though
476+
// the code executing is not even running inside a Task.
477+
//
478+
// Note that this only works because the closure in assumeIsolated is
479+
// synchronous, and will not cause suspensions, as that would require the
480+
// presence of a Task.
481+
// compat_invoke_swift_task_checkIsolated(expectedExecutor);
482+
if (isCurrentExecutorMode == Default_UseCheckIsolated_AllowCrash) {
483+
swift_task_checkIsolated(expectedExecutor);
484+
// The checkIsolated call did not crash, so we are on the right executor.
485+
return true;
347486
}
348487

488+
// Using legacy mode, if no explicit executor match worked, we assume `false`
489+
assert(isCurrentExecutorMode == Legacy_NoCheckIsolated_NonCrashing);
349490
return false;
350491
}
351492

352493
/// Logging level for unexpected executors:
353494
/// 0 - no logging
354495
/// 1 - warn on each instance
355496
/// 2 - fatal error
356-
static unsigned unexpectedExecutorLogLevel = 2;
497+
///
498+
/// NOTE: The default behavior on Apple platforms depends on the SDK version
499+
/// an application was linked to. Since Swift 6 the default is to crash,
500+
/// and the logging behavior is no longer available.
501+
static unsigned unexpectedExecutorLogLevel =
502+
swift_bincompat_useLegacyNonCrashingExecutorChecks()
503+
? 1 // legacy apps default to the logging mode, and cannot use `checkIsolated`
504+
: 2; // new apps will only crash upon concurrency violations, and will call into `checkIsolated`
357505

358506
static void checkUnexpectedExecutorLogLevel(void *context) {
359507
#if SWIFT_STDLIB_HAS_ENVIRON

stdlib/public/Concurrency/CooperativeGlobalExecutor.inc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/// swift_task_enqueueGlobalImpl
1919
/// swift_task_enqueueGlobalWithDelayImpl
2020
/// swift_task_enqueueMainExecutorImpl
21+
/// swift_task_checkIsolatedImpl
2122
/// as well as any cooperative-executor-specific functions in the runtime.
2223
///
2324
///===------------------------------------------------------------------===///
@@ -136,6 +137,13 @@ static void insertDelayedJob(Job *newJob, JobDeadline deadline) {
136137
*position = newJob;
137138
}
138139

140+
SWIFT_CC(swift)
141+
static void swift_task_checkIsolatedImpl(SerialExecutorRef executor) {
142+
_task_serialExecutor_checkIsolated(
143+
executor.getIdentity(), swift_getObjectType(executor.getIdentity()),
144+
executor.getSerialExecutorWitnessTable());
145+
}
146+
139147
/// Insert a job into the cooperative global queue with a delay.
140148
SWIFT_CC(swift)
141149
static void swift_task_enqueueGlobalWithDelayImpl(JobDelay delay,

0 commit comments

Comments
 (0)