Skip to content

[Concurrency] Further prevent crashes in legacy mode of isSameExecutor #73813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 28, 2024
180 changes: 106 additions & 74 deletions stdlib/public/Concurrency/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ bool _task_serialExecutor_isSameExclusiveExecutionContext(
enum IsCurrentExecutorCheckMode: unsigned {
/// The default mode when an app was compiled against "new" enough SDK.
/// It allows crashing in isCurrentExecutor, and calls into `checkIsolated`.
Default_UseCheckIsolated_AllowCrash,
Swift6_UseCheckIsolated_AllowCrash,
/// Legacy mode; Primarily to support old applications which used data race
/// detector with "warning" mode, which is no longer supported. When such app
/// is re-compiled against a new SDK, it will see crashes in what was
Expand All @@ -332,39 +332,45 @@ enum IsCurrentExecutorCheckMode: unsigned {
Legacy_NoCheckIsolated_NonCrashing,
};
static IsCurrentExecutorCheckMode isCurrentExecutorMode =
Default_UseCheckIsolated_AllowCrash;

Swift6_UseCheckIsolated_AllowCrash;

// Shimming call to Swift runtime because Swift Embedded does not have
// these symbols defined
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
// these symbols defined.
bool __swift_bincompat_useLegacyNonCrashingExecutorChecks() {
#if !SWIFT_CONCURRENCY_EMBEDDED
swift::runtime::bincompat::
return swift::runtime::bincompat::
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this patch also includes this very important bit; previously this missing return would have rendered the bincompat detection useless.

swift_bincompat_useLegacyNonCrashingExecutorChecks();
#endif
#else
return false;
#endif
}

// Check override of executor checking mode.
static void checkIsCurrentExecutorMode(void *context) {
auto useLegacyMode =
swift_bincompat_useLegacyNonCrashingExecutorChecks();
// Done this way because of the interaction with the initial value of
// 'unexpectedExecutorLogLevel'
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
bool legacyMode = __swift_bincompat_useLegacyNonCrashingExecutorChecks();

// Potentially, override the platform detected mode, primarily used in tests.
#if SWIFT_STDLIB_HAS_ENVIRON
if (const char *modeStr =
runtime::environment::concurrencyIsCurrentExecutorLegacyModeOverride()) {
if (modeStr) {
if (strcmp(modeStr, "nocrash") == 0) {
useLegacyMode = true;
} else if (strcmp(modeStr, "crash") == 0) {
useLegacyMode = false;
} // else, just use the platform detected mode
}
}
if (const char *modeStr = runtime::environment::
concurrencyIsCurrentExecutorLegacyModeOverride()) {
if (strcmp(modeStr, "nocrash") == 0 || strcmp(modeStr, "legacy") == 0) {
return true;
} else if (strcmp(modeStr, "crash") == 0 || strcmp(modeStr, "swift6") == 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to make it less confusing in our tests; this is not expected to be used by anyone tbh, but I keep the old mode names.

return false; // don't use the legacy mode
} // else, just use the platform detected mode
} // no override, use the default mode
#endif // SWIFT_STDLIB_HAS_ENVIRON

return legacyMode;
}

// Check override of executor checking mode.
static void checkIsCurrentExecutorMode(void *context) {
bool useLegacyMode =
swift_bincompat_useLegacyNonCrashingExecutorChecks();
isCurrentExecutorMode = useLegacyMode ? Legacy_NoCheckIsolated_NonCrashing
: Default_UseCheckIsolated_AllowCrash;
: Swift6_UseCheckIsolated_AllowCrash;
}

SWIFT_CC(swift)
Expand All @@ -373,6 +379,12 @@ static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor)

// To support old applications on apple platforms which assumed this call
// does not crash, try to use a more compatible mode for those apps.
//
// We only allow returning `false` directly from this function when operating
// in 'Legacy_NoCheckIsolated_NonCrashing' mode. If allowing crashes, we
// instead must call into 'checkIsolated' or crash directly.
//
// Whenever we confirm an executor equality, we can return true, in any mode.
static swift::once_t checkModeToken;
swift::once(checkModeToken, checkIsCurrentExecutorMode, nullptr);

Expand All @@ -382,20 +394,19 @@ static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor)
// the expected executor however, so we need to try a bit harder before
// we fail.

// Are we expecting the main executor and are using the main thread?
// Special handling the main executor by detecting the main thread.
if (expectedExecutor.isMainExecutor() && isExecutingOnMainThread()) {
// Due to compatibility with pre-checkIsolated code, we cannot remove
// this special handling. CheckIsolated can handle this if the expected
// executor is the main queue / main executor, however, if we cannot call
// checkIsolated we cannot rely on it to handle this.
// TODO: consider removing this branch when `useCrashingCheckIsolated=true`
return true;
}

// We cannot use 'complexEquality' as it requires two executor instances,
// and we do not have a 'current' executor here.

// Otherwise, as last resort, let the expected executor check using
// external means, as it may "know" this thread is managed by it etc.
if (isCurrentExecutorMode == Default_UseCheckIsolated_AllowCrash) {
swift_task_checkIsolated(expectedExecutor);
if (isCurrentExecutorMode == Swift6_UseCheckIsolated_AllowCrash) {
swift_task_checkIsolated(expectedExecutor); // will crash if not same context

// checkIsolated did not crash, so we are on the right executor, after all!
return true;
}
Expand All @@ -418,46 +429,50 @@ static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor)
return true;
}

// If the expected executor is "default" then we should have matched
// by pointer equality already with the current executor.
if (expectedExecutor.isDefaultActor()) {
// If the expected executor is a default actor, it makes no sense to try
// the 'checkIsolated' call, it must be equal to the other actor, or it is
// not the same isolation domain.
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption");
return false;
}

if (expectedExecutor.isMainExecutor() && !currentExecutor.isMainExecutor()) {
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected MainActor executor");
return false;
} else if (!expectedExecutor.isMainExecutor() && currentExecutor.isMainExecutor()) {
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected not-MainActor executor");
return false;
}

if (expectedExecutor.isComplexEquality()) {
if (!swift_compareWitnessTables(
reinterpret_cast<const WitnessTable*>(currentExecutor.getSerialExecutorWitnessTable()),
reinterpret_cast<const WitnessTable*>(expectedExecutor.getSerialExecutorWitnessTable()))) {
// different witness table, we cannot invoke complex equality call
// Only in legacy mode:
// We check if the current xor expected executor are the main executor.
// If so only one of them is, we know that WITHOUT 'checkIsolated' or invoking
// 'dispatch_assert_queue' we cannot be truly sure the expected/current truly
// are "on the same queue". There exists no non-crashing API to check this,
// so we PESSIMISTICALLY return false here.
//
// In Swift6 mode:
// We don't do this naive check, because we'll fall back to
// `expected.checkIsolated()` which, if it is the main executor, will invoke
// the crashing 'dispatch_assert_queue(main queue)' which will either crash
// or confirm we actually are on the main queue; or the custom expected
// executor has a chance to implement a similar queue check.
if (isCurrentExecutorMode == Legacy_NoCheckIsolated_NonCrashing) {
if ((expectedExecutor.isMainExecutor() && !currentExecutor.isMainExecutor()) ||
(!expectedExecutor.isMainExecutor() && currentExecutor.isMainExecutor())) {
return false;
}
}

// Avoid passing nulls to Swift for the isSame check:
if (!currentExecutor.getIdentity() || !expectedExecutor.getIdentity()) {
return false;
// Complex equality means that if two executors of the same type have some
// special logic to check if they are "actually the same".
if (expectedExecutor.isComplexEquality()) {
if (currentExecutor.getIdentity() &&
expectedExecutor.getIdentity() &&
swift_compareWitnessTables(
reinterpret_cast<const WitnessTable *>(
currentExecutor.getSerialExecutorWitnessTable()),
reinterpret_cast<const WitnessTable *>(
expectedExecutor.getSerialExecutorWitnessTable()))) {

auto isSameExclusiveExecutionContextResult =
_task_serialExecutor_isSameExclusiveExecutionContext(
currentExecutor.getIdentity(), expectedExecutor.getIdentity(),
swift_getObjectType(currentExecutor.getIdentity()),
expectedExecutor.getSerialExecutorWitnessTable());

// if the 'isSameExclusiveExecutionContext' returned true we trust
// it and return; if it was false, we need to give checkIsolated another
// chance to check.
if (isSameExclusiveExecutionContextResult) {
return true;
} // else, we must give 'checkIsolated' a last chance to verify isolation
}

auto result = _task_serialExecutor_isSameExclusiveExecutionContext(
currentExecutor.getIdentity(),
expectedExecutor.getIdentity(),
swift_getObjectType(currentExecutor.getIdentity()),
expectedExecutor.getSerialExecutorWitnessTable());

return result;
}

// This provides a last-resort check by giving the expected SerialExecutor the
Expand All @@ -480,21 +495,22 @@ static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor)
// Note that this only works because the closure in assumeIsolated is
// synchronous, and will not cause suspensions, as that would require the
// presence of a Task.
// compat_invoke_swift_task_checkIsolated(expectedExecutor);
if (isCurrentExecutorMode == Default_UseCheckIsolated_AllowCrash) {
swift_task_checkIsolated(expectedExecutor);
if (isCurrentExecutorMode == Swift6_UseCheckIsolated_AllowCrash) {
swift_task_checkIsolated(expectedExecutor); // will crash if not same context

// The checkIsolated call did not crash, so we are on the right executor.
return true;
}

// Using legacy mode, if no explicit executor match worked, we assume `false`
// In the end, since 'checkIsolated' could not be used, so we must assume
// that the executors are not the same context.
assert(isCurrentExecutorMode == Legacy_NoCheckIsolated_NonCrashing);
return false;
}

/// Logging level for unexpected executors:
/// 0 - no logging
/// 1 - warn on each instance
/// 0 - no logging -- will be IGNORED when Swift6 mode of isCurrentExecutor is used
/// 1 - warn on each instance -- will be IGNORED when Swift6 mode of isCurrentExecutor is used
/// 2 - fatal error
///
/// NOTE: The default behavior on Apple platforms depends on the SDK version
Expand All @@ -512,8 +528,24 @@ static void checkUnexpectedExecutorLogLevel(void *context) {
return;

long level = strtol(levelStr, nullptr, 0);
if (level >= 0 && level < 3)
unexpectedExecutorLogLevel = level;
if (level >= 0 && level < 3) {
if (swift_bincompat_useLegacyNonCrashingExecutorChecks()) {
// legacy mode permits doing nothing or just logging, since the method
// used to perform the check itself is not going to crash:
unexpectedExecutorLogLevel = level;
} else {
// We are in swift6/crash mode of isCurrentExecutor which means that
// rather than returning false, that method will always CRASH when an
// executor mismatch is discovered.
//
// Thus, for clarity, we set this mode also to crashing, as runtime should
// not expect to be able to get any logging or ignoring done. In practice,
// the crash would happen before logging or "ignoring", but this should
// help avoid confusing situations like "I thought it should log" when
// debugging the runtime.
unexpectedExecutorLogLevel = 2;
}
}
#endif // SWIFT_STDLIB_HAS_ENVIRON
}

Expand Down
13 changes: 13 additions & 0 deletions stdlib/public/runtime/Bincompat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,19 @@ bool useLegacySwiftObjCHashing() {
#endif
}

// Controls legacy mode for the 'swift_task_isCurrentExecutorImpl' runtime function.
//
// In "legacy" / "no crash" mode:
// * The `swift_task_isCurrentExecutorImpl` cannot crash
// * This means cases where no "current" executor is present cannot be diagnosed correctly
// * The runtime can NOT use 'SerialExecutor/checkIsolated'
// * The runtime can NOT use 'dispatch_precondition' which is able ot handle some dispatch and main actor edge cases
//
// New behavior in "swift6" "crash" mode:
// * The 'swift_task_isCurrentExecutorImpl' will CRASH rather than return 'false'
// * This allows the method to invoke 'SerialExecutor/checkIsolated'
// * Which is allowed to call 'dispatch_precondition' and handle "on dispatch queue but not on Swift executor" cases
//
// FIXME(concurrency): Once the release is announced, adjust the logic detecting the SDKs
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
#if BINARY_COMPATIBILITY_APPLE
Expand Down
4 changes: 2 additions & 2 deletions stdlib/public/runtime/EnvironmentVariables.def
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ VARIABLE(SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE, string, "",
"non-crashing behavior. This flag enables temporarily restoring the "
"legacy 'nocrash' behavior until adopting code has been adjusted. "
"Legal values are: "
" 'nocrash' (Legacy behavior), "
" 'crash' (Swift 6.0+ behavior)")
" 'legacy' (Legacy behavior), "
" 'swift6' (Swift 6.0+ behavior)")

#undef VARIABLE
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
// RUN: %empty-directory(%t)
// RUN: %target-build-swift -Xfrontend -disable-availability-checking -parse-as-library %s -o %t/a.out
// RUN: %target-codesign %t/a.out
// RUN: %target-run %t/a.out
// RUN: %env-SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE=legacy %target-run %t/a.out

// REQUIRES: executable_test
// REQUIRES: concurrency
// REQUIRES: concurrency_runtime

// TODO: The actual reason is that we do these %env- tricks, which e.g. Windows is confused about
// REQUIRES: libdispatch

// UNSUPPORTED: back_deployment_runtime
// UNSUPPORTED: back_deploy_concurrency
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: freestanding

// rdar://119743909 fails in optimze tests.
// rdar://119743909 fails in optimize tests.
// UNSUPPORTED: swift_test_mode_optimize
// UNSUPPORTED: swift_test_mode_optimize_size

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// RUN: %target-run-simple-swift(-parse-as-library -Xfrontend -disable-availability-checking) | %FileCheck %s
// RUN: %empty-directory(%t)
// RUN: %target-build-swift -Xfrontend -disable-availability-checking %import-libdispatch -parse-as-library %s -o %t/a.out
// RUN: %target-codesign %t/a.out
// RUN: %env-SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE=swift6 %target-run %t/a.out
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that all the tests now have two SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE versions


// REQUIRES: executable_test
// REQUIRES: concurrency
Expand All @@ -11,6 +14,8 @@
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: freestanding

import StdlibUnittest

final class NaiveQueueExecutor: SerialExecutor {
init() {}

Expand Down Expand Up @@ -38,32 +43,26 @@ actor ActorOnNaiveQueueExecutor {
self.executor.asUnownedSerialExecutor()
}

// Executes on global pool, but our `checkIsolated` impl pretends
// that it is the same executor by never crashing.
nonisolated func checkPreconditionIsolated() async {
print("Before preconditionIsolated")
self.preconditionIsolated()
print("After preconditionIsolated")

print("Before assumeIsolated")
self.assumeIsolated { iso in
print("Inside assumeIsolated")
}
print("After assumeIsolated")
}
}

@main struct Main {
static func main() async {
if #available(SwiftStdlib 6.0, *) {
let actor = ActorOnNaiveQueueExecutor()
await actor.checkPreconditionIsolated()
// CHECK: Before preconditionIsolated
// CHECK-NEXT: checkIsolated: pretend it is ok!
// CHECK-NEXT: After preconditionIsolated
let tests = TestSuite("AssertPreconditionIsolationTests")

// CHECK-NEXT: Before assumeIsolated
// CHECK-NEXT: checkIsolated: pretend it is ok!
// CHECK-NEXT: Inside assumeIsolated
// CHECK-NEXT: After assumeIsolated
if #available(SwiftStdlib 6.0, *) {
tests.test("[swift6+checkIsolated] Isolation assured by invoking 'checkIsolated'") {
let actor = ActorOnNaiveQueueExecutor()
await actor.checkPreconditionIsolated()
}
}

await runAllTestsAsync()
}
}
Loading