Skip to content

Commit 325b66a

Browse files
committed
[Concurrency] Fix alreadyLocked in withStatusRecordLock.
If the preloaded status is locked, then we need to reload it in order to distinguish between the current thread holding the lock and another thread holding the lock. Without this, if another thread holds the lock, then we won't set the is-locked bit. We'll still actually hold the lock, but other threads may perform operations locklessly if the bit is not set, which can cause a crash. By reloading status in that case, we ensure that the bit is always set correctly. This manifested as crashes in task cancellation but could cause other task-related issues as well. Also remove an assert of !isStatusRecordLocked() in AsyncTask::complete(). We allow other threads to access tasks and take the lock for things like cancellation, so the lock may legitimately be held at that point. rdar://150327908
1 parent 1c1f43f commit 325b66a

File tree

3 files changed

+50
-4
lines changed

3 files changed

+50
-4
lines changed

stdlib/public/Concurrency/TaskPrivate.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -829,11 +829,12 @@ struct AsyncTask::PrivateStorage {
829829
// result of the task
830830
auto oldStatus = task->_private()._status().load(std::memory_order_relaxed);
831831
while (true) {
832-
// Task is completing
833832
assert(oldStatus.getInnermostRecord() == NULL &&
834833
"Status records should have been removed by this time!");
835-
assert(!oldStatus.isStatusRecordLocked() &&
836-
"Task is completing, cannot be locked anymore!");
834+
835+
// Don't assert !isStatusRecordLocked(). It can be legitimately true here,
836+
// for example if another thread is canceling this task right as it
837+
// completes.
837838

838839
assert(oldStatus.isRunning());
839840

stdlib/public/Concurrency/TaskStatus.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,19 @@ static void withStatusRecordLock(
6767
// see that the is-locked bit is now set, and then wait for the lock.
6868
task->_private().statusLock.lock();
6969

70-
bool alreadyLocked = status.isStatusRecordLocked();
70+
bool alreadyLocked = false;
71+
72+
// `status` was loaded before we acquired the lock. If its is-locked bit is
73+
// not set, then we know that this thread doesn't already hold the lock.
74+
// However, if the is-locked bit is set, then we don't know if this thread
75+
// held the lock or another thread did. In that case, we reload the status
76+
// after acquiring the lock. If the reloaded status still has the is-locked
77+
// bit set, then we know it's this thread. If it doesn't, then we know it was
78+
// a different thread.
79+
if (status.isStatusRecordLocked()) {
80+
status = task->_private()._status().load(std::memory_order_relaxed);
81+
alreadyLocked = status.isStatusRecordLocked();
82+
}
7183

7284
// If it's already locked then this thread is the thread that locked it, and
7385
// we can leave that bit alone here.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// RUN: %target-run-simple-swift( -target %target-swift-5.1-abi-triple %import-libdispatch)
2+
// REQUIRES: concurrency
3+
// REQUIRES: executable_test
4+
5+
// REQUIRES: concurrency_runtime
6+
// UNSUPPORTED: freestanding
7+
8+
func recurseABunch(_ call: () async throws -> Void, n: Int = 100) async throws {
9+
try await withTaskCancellationHandler {
10+
if n == 0 {
11+
try await call()
12+
return
13+
}
14+
15+
try await recurseABunch(call, n: n - 1)
16+
} onCancel: {
17+
}
18+
}
19+
20+
for _ in 0..<100_000 {
21+
let task: Task<Void, any Error> = Task {
22+
try await Task.sleep(nanoseconds: UInt64.random(in: 0..<1_000))
23+
try await recurseABunch() {
24+
await withTaskCancellationHandler {
25+
} onCancel: {
26+
}
27+
}
28+
}
29+
Task {
30+
try await Task.sleep(nanoseconds: UInt64.random(in: 0..<1_000))
31+
task.cancel()
32+
}
33+
}

0 commit comments

Comments
 (0)