Skip to content

Commit e10ad6b

Browse files
committed
[Concurrency] Avoid inserting handler record in already cancelled task.
This avoids the potential to race with the triggering coming from task_cancel, because we first set the cancelled flag, and only THEN take the lock and iterate over the inserted records. Because of this we could: T1 flip the cancelled bit; T2 observes that, and triggers "immediately" during installing the handler record. T1 then proceeds to lock records and trigger it again, causing a double trigger of the cancellation handler. resolves swiftlang#80161 resolves rdar://147493150
1 parent a1cdd33 commit e10ad6b

File tree

4 files changed

+107
-10
lines changed

4 files changed

+107
-10
lines changed

stdlib/public/Concurrency/Task.cpp

+23-5
Original file line numberDiff line numberDiff line change
@@ -1758,27 +1758,45 @@ swift_task_addCancellationHandlerImpl(
17581758
CancellationNotificationStatusRecord(unsigned_handler, context);
17591759

17601760
bool fireHandlerNow = false;
1761-
17621761
addStatusRecordToSelf(record, [&](ActiveTaskStatus oldStatus, ActiveTaskStatus& newStatus) {
17631762
if (oldStatus.isCancelled()) {
1764-
fireHandlerNow = true;
17651763
// We don't fire the cancellation handler here since this function needs
17661764
// to be idempotent
1765+
fireHandlerNow = true;
1766+
1767+
// don't add the record, because that would risk triggering it from
1768+
// task_cancel, concurrently with the record->run() we're about to do below.
1769+
return false;
17671770
}
1768-
return true;
1771+
return true; // add the record
17691772
});
17701773

17711774
if (fireHandlerNow) {
17721775
record->run();
1776+
1777+
// we have not added the record to the task because it has fired immediately,
1778+
// and therefore we can clean it up immediately rather than wait until removeCancellationHandler
1779+
// which would be triggered at the end of the withTaskCancellationHandler block.
1780+
swift_task_dealloc(record);
1781+
return nullptr; // indicate to the remove... method, that there was no task added
17731782
}
17741783
return record;
17751784
}
17761785

17771786
SWIFT_CC(swift)
17781787
static void swift_task_removeCancellationHandlerImpl(
17791788
CancellationNotificationStatusRecord *record) {
1780-
removeStatusRecordFromSelf(record);
1781-
swift_task_dealloc(record);
1789+
if (!record) {
1790+
// seems we never added the record but have run it immediately,
1791+
// so we make no attempts to remove it.
1792+
return;
1793+
}
1794+
1795+
if (auto poppedRecord =
1796+
popStatusRecordOfType<CancellationNotificationStatusRecord>(swift_task_getCurrent())) {
1797+
assert(record == poppedRecord && "The removed record did not match the expected record!");
1798+
swift_task_dealloc(record);
1799+
}
17821800
}
17831801

17841802
SWIFT_CC(swift)

stdlib/public/Concurrency/TaskPrivate.h

+10
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,16 @@ void removeStatusRecordWhere(
244244
llvm::function_ref<bool(ActiveTaskStatus, TaskStatusRecord*)> condition,
245245
llvm::function_ref<void(ActiveTaskStatus, ActiveTaskStatus&)>updateStatus = nullptr);
246246

247+
/// Remove and return a status record of the given type. This function removes a
248+
/// singlw record, and leaves subsequent records as-is if there are any.
249+
/// Returns `nullptr` if there are no matching records.
250+
///
251+
/// NOTE: When using this function with new record types, make sure to provide
252+
/// an explicit instantiation in TaskStatus.cpp.
253+
template <typename TaskStatusRecordT>
254+
SWIFT_CC(swift)
255+
TaskStatusRecordT* popStatusRecordOfType(AsyncTask *task);
256+
247257
/// Remove a status record from the current task. This must be called
248258
/// synchronously with the task.
249259
SWIFT_CC(swift)

stdlib/public/Concurrency/TaskStatus.cpp

+12-5
Original file line numberDiff line numberDiff line change
@@ -350,15 +350,18 @@ void swift::removeStatusRecordWhere(
350350
});
351351
}
352352

353-
// Remove and return a status record of the given type. There must be at most
354-
// one matching record. Returns nullptr if there are none.
355353
template <typename TaskStatusRecordT>
356-
static TaskStatusRecordT *popStatusRecordOfType(AsyncTask *task) {
354+
SWIFT_CC(swift)
355+
TaskStatusRecordT* swift::popStatusRecordOfType(AsyncTask *task) {
357356
TaskStatusRecordT *record = nullptr;
357+
bool alreadyRemovedRecord = false;
358358
removeStatusRecordWhere(task, [&](ActiveTaskStatus s, TaskStatusRecord *r) {
359+
if (alreadyRemovedRecord)
360+
return false;
361+
359362
if (auto *match = dyn_cast<TaskStatusRecordT>(r)) {
360-
assert(!record && "two matching records found");
361363
record = match;
364+
alreadyRemovedRecord = true;
362365
return true; // Remove this record.
363366
}
364367

@@ -562,6 +565,10 @@ static void swift_task_popTaskExecutorPreferenceImpl(
562565
swift_task_dealloc(record);
563566
}
564567

568+
// Since the header would have incomplete declarations, we instead instantiate a concrete version of the function here
569+
template SWIFT_CC(swift)
570+
CancellationNotificationStatusRecord* swift::popStatusRecordOfType<CancellationNotificationStatusRecord>(AsyncTask *);
571+
565572
void AsyncTask::pushInitialTaskExecutorPreference(
566573
TaskExecutorRef preferredExecutor, bool owned) {
567574
void *allocation = _swift_task_alloc_specific(
@@ -879,7 +886,7 @@ static void swift_task_cancelImpl(AsyncTask *task) {
879886
}
880887

881888
newStatus.traceStatusChanged(task, false);
882-
if (newStatus.getInnermostRecord() == NULL) {
889+
if (newStatus.getInnermostRecord() == nullptr) {
883890
// No records, nothing to propagate
884891
return;
885892
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -target %target-swift-5.1-abi-triple %import-libdispatch) | %FileCheck %s
2+
// REQUIRES: concurrency
3+
// REQUIRES: executable_test
4+
5+
// rdar://76038845
6+
// REQUIRES: concurrency_runtime
7+
// UNSUPPORTED: back_deployment_runtime
8+
// UNSUPPORTED: freestanding
9+
10+
import Synchronization
11+
12+
struct State {
13+
var cancelled = 0
14+
var continuation: CheckedContinuation<Void, Never>?
15+
}
16+
17+
func testFunc(_ iteration: Int) async -> Task<Void, Never> {
18+
let state = Mutex(State())
19+
20+
let task = Task {
21+
await withTaskCancellationHandler {
22+
await withCheckedContinuation { continuation in
23+
let cancelled = state.withLock {
24+
if $0.cancelled > 0 {
25+
return true
26+
} else {
27+
$0.continuation = continuation
28+
return false
29+
}
30+
}
31+
if cancelled {
32+
continuation.resume()
33+
}
34+
}
35+
} onCancel: {
36+
let continuation = state.withLock {
37+
$0.cancelled += 1
38+
return $0.continuation.take()
39+
}
40+
continuation?.resume()
41+
}
42+
}
43+
44+
// This task cancel is racing with installing the cancellation handler,
45+
// and we may either hit the cancellation handler:
46+
// - after this cancel was issued, and therefore the handler runs immediately
47+
task.cancel()
48+
_ = await task.value
49+
50+
let cancelled = state.withLock { $0.cancelled }
51+
precondition(cancelled == 1, "cancelled more than once, iteration: \(iteration)")
52+
53+
return task
54+
}
55+
56+
var ts: [Task<Void, Never>] = []
57+
for iteration in 0..<1_000 {
58+
let t = await testFunc(iteration)
59+
ts.append(t)
60+
}
61+
62+
print("done") // CHECK: done

0 commit comments

Comments
 (0)