Skip to content

Commit 0c44645

Browse files
authored
[Concurrency] Implement defensive copying in task groups, rather than crashing (#73978)
1 parent f69e3b3 commit 0c44645

14 files changed

+455
-138
lines changed

include/swift/ABI/TaskLocal.h

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class TaskLocal {
4444
/// lookups by skipping empty parent tasks during get(), and explained
4545
/// in depth in `createParentLink`.
4646
IsParent = 0b01,
47+
/// The task local binding was created inside the body of a `withTaskGroup`,
48+
/// and therefore must either copy it, or crash when a child task is created
49+
/// using 'group.addTask' and it would refer to this task local.
50+
///
51+
/// Items of this kind must be copied by a group child task for access
52+
/// safety reasons, as otherwise the pop would happen before the child task
53+
/// has completed.
54+
IsNextCreatedInTaskGroupBody = 0b10,
4755
};
4856

4957
class Item {
@@ -101,20 +109,55 @@ class TaskLocal {
101109
/// the Item linked list into the appropriate parent.
102110
static Item *createParentLink(AsyncTask *task, AsyncTask *parent);
103111

112+
static Item *createLink(AsyncTask *task,
113+
const HeapObject *key,
114+
const Metadata *valueType,
115+
bool inTaskGroupBody);
116+
104117
static Item *createLink(AsyncTask *task,
105118
const HeapObject *key,
106119
const Metadata *valueType);
107120

121+
static Item *createLinkInTaskGroup(
122+
AsyncTask *task,
123+
const HeapObject *key,
124+
const Metadata *valueType);
125+
108126
void destroy(AsyncTask *task);
109127

110128
Item *getNext() {
111129
return reinterpret_cast<Item *>(next & ~statusMask);
112130
}
113131

132+
void relinkNext(Item* nextOverride) {
133+
assert(!getNext() &&
134+
"Can only relink task local item that was not pointing at anything yet");
135+
assert(nextOverride->isNextLinkPointer() ||
136+
nextOverride->isParentPointer() &&
137+
"Currently relinking is only done within a task group to "
138+
"avoid within-taskgroup next pointers; attempted to point at "
139+
"task local declared within task group body though!");
140+
141+
next = reinterpret_cast<uintptr_t>(nextOverride) |
142+
static_cast<uintptr_t>((nextOverride->isNextLinkPointer()
143+
? NextLinkType::IsNextCreatedInTaskGroupBody
144+
: NextLinkType::IsParent));
145+
}
146+
114147
NextLinkType getNextLinkType() const {
115148
return static_cast<NextLinkType>(next & statusMask);
116149
}
117150

151+
bool isNextLinkPointer() const {
152+
return static_cast<NextLinkType>(next & statusMask) ==
153+
NextLinkType::IsNext;
154+
}
155+
156+
bool isNextLinkPointerCreatedInTaskGroupBody() const {
157+
return static_cast<NextLinkType>(next & statusMask) ==
158+
NextLinkType::IsNextCreatedInTaskGroupBody;
159+
}
160+
118161
/// Item does not contain any actual value, and is only used to point at
119162
/// a specific parent item.
120163
bool isEmpty() const {
@@ -127,7 +170,7 @@ class TaskLocal {
127170
reinterpret_cast<char *>(this) + storageOffset(valueType));
128171
}
129172

130-
void copyTo(AsyncTask *task);
173+
TaskLocal::Item* copyTo(AsyncTask *task);
131174

132175
/// Compute the offset of the storage from the base of the item.
133176
static size_t storageOffset(const Metadata *valueType) {
@@ -136,9 +179,9 @@ class TaskLocal {
136179
if (valueType) {
137180
size_t alignment = valueType->vw_alignment();
138181
return (offset + alignment - 1) & ~(alignment - 1);
139-
} else {
140-
return offset;
141182
}
183+
184+
return offset;
142185
}
143186

144187
/// Determine the size of the item given a particular value type.
@@ -200,6 +243,9 @@ class TaskLocal {
200243
/// can be safely disposed of.
201244
bool popValue(AsyncTask *task);
202245

246+
/// Peek at the head item and get its type.
247+
std::optional<NextLinkType> peekHeadLinkType() const;
248+
203249
/// Copy all task-local bindings to the target task.
204250
///
205251
/// The new bindings allocate their own items and can out-live the current task.
@@ -212,6 +258,8 @@ class TaskLocal {
212258
/// "pop" of the `B` value - it was spawned from a scope where only B was observable.
213259
void copyTo(AsyncTask *target);
214260

261+
void copyToOnlyOnlyFromCurrent(AsyncTask *target);
262+
215263
/// Destroy and deallocate all items stored by this specific task.
216264
///
217265
/// Items owned by a parent task are left untouched, since we do not own them.

include/swift/Runtime/Concurrency.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,11 +663,23 @@ void swift_task_localValuePop();
663663
/// Its Swift signature is
664664
///
665665
/// \code
666-
/// func _taskLocalValueGet<Key>(AsyncTask* task)
666+
/// func swift_task_localsCopyTo<Key>(AsyncTask* task)
667667
/// \endcode
668668
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
669669
void swift_task_localsCopyTo(AsyncTask* target);
670670

671+
/// Some task local bindings must be copied defensively when a child task is
672+
/// created in a task group. See task creation (swift_task_create_common) for
673+
/// a detailed discussion how and when this is used.
674+
///
675+
/// Its Swift signature is
676+
///
677+
/// \code
678+
/// func swift_task_localsCopyToTaskGroupChildTaskDefensively<Key>(AsyncTask* task)
679+
/// \endcode
680+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
681+
void swift_task_localsCopyToTaskGroupChildTaskDefensively(AsyncTask* target);
682+
671683
/// Switch the current task to a new executor if we aren't already
672684
/// running on a compatible executor.
673685
///

stdlib/public/CompatibilityOverride/CompatibilityOverrideConcurrency.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,12 @@ OVERRIDE_TASK_LOCAL(task_localsCopyTo, void,
378378
(AsyncTask *target),
379379
(target))
380380

381+
OVERRIDE_TASK_LOCAL(task_localsCopyToTaskGroupChildTaskDefensively, void,
382+
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
383+
swift::,
384+
(AsyncTask *target),
385+
(target))
386+
381387
OVERRIDE_TASK_STATUS(task_hasTaskGroupStatusRecord, bool,
382388
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
383389
swift::, , )

stdlib/public/Concurrency/Actor.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,6 @@ const char *__swift_runtime_env_useLegacyNonCrashingExecutorChecks() {
357357
#endif
358358
}
359359

360-
#pragma clang diagnostic push
361-
#pragma ide diagnostic ignored "ConstantConditionsOC"
362360
// Done this way because of the interaction with the initial value of
363361
// 'unexpectedExecutorLogLevel'
364362
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
@@ -378,7 +376,6 @@ bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
378376

379377
return legacyMode;
380378
}
381-
#pragma clang diagnostic pop
382379

383380
// Check override of executor checking mode.
384381
static void checkIsCurrentExecutorMode(void *context) {

stdlib/public/Concurrency/Task.cpp

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -729,17 +729,16 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
729729
assert(initialContextSize >= sizeof(FutureAsyncContext));
730730
}
731731

732-
// Add to the task group, if requested.
733-
if (taskCreateFlags.addPendingGroupTaskUnconditionally()) {
734-
assert(group && "Missing group");
735-
swift_taskGroup_addPending(group, /*unconditionally=*/true);
736-
}
737-
738-
AsyncTask *parent = nullptr;
739732
AsyncTask *currentTask = swift_task_getCurrent();
740-
if (jobFlags.task_isChildTask()) {
741-
parent = currentTask;
742-
assert(parent != nullptr && "creating a child task with no active task");
733+
AsyncTask *parent = jobFlags.task_isChildTask() ? currentTask : nullptr;
734+
735+
if (group) {
736+
assert(parent && "a task created in a group must be a child task");
737+
// Add to the task group, if requested.
738+
if (taskCreateFlags.addPendingGroupTaskUnconditionally()) {
739+
assert(group && "Missing group");
740+
swift_taskGroup_addPending(group, /*unconditionally=*/true);
741+
}
743742
}
744743

745744
// Start with user specified priority at creation time (if any)
@@ -983,12 +982,51 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
983982
// In a task group we would not have allowed the `add` to create a child anymore,
984983
// however better safe than sorry and `async let` are not expressed as task groups,
985984
// so they may have been spawned in any case still.
986-
if (swift_task_isCancelled(parent) ||
987-
(group && group->isCancelled()))
985+
if ((group && group->isCancelled()) || swift_task_isCancelled(parent))
988986
swift_task_cancel(task);
989987

990-
// Initialize task locals with a link to the parent task.
991-
task->_private().Local.initializeLinkParent(task, parent);
988+
// Initialize task locals storage
989+
bool taskLocalStorageInitialized = false;
990+
991+
// Inside a task group, we may have to perform some defensive copying,
992+
// check if doing so is necessary, and initialize storage using partial
993+
// defensive copies if necessary.
994+
if (group) {
995+
assert(parent && "a task created in a group must be a child task");
996+
// We are a child task in a task group; and it may happen that we are calling
997+
// addTask specifically in such shape:
998+
//
999+
// $local.withValue(theValue) { addTask {} }
1000+
//
1001+
// If this is the case, we MUST copy `theValue` (and any other such directly
1002+
// wrapping the addTask value bindings), because those values will be popped
1003+
// when withValue returns - breaking our structured concurrency guarantees
1004+
// that we rely on for the "link directly to parent's task local Item".
1005+
//
1006+
// Values set outside the task group are not subject to this problem, as
1007+
// their structural lifetime guarantee is upheld by the group scope
1008+
// out-living any addTask created tasks.
1009+
auto ParentLocal = parent->_private().Local;
1010+
// If we were going to copy ALL values anyway, we don't need to
1011+
// perform this defensive partial copying. In practice, we currently
1012+
// do not have child tasks which force copying, but we could.
1013+
assert(!taskCreateFlags.copyTaskLocals() &&
1014+
"Currently we don't have child tasks which force copying task "
1015+
"locals; unexpected attempt to combine the two!");
1016+
1017+
if (auto taskLocalHeadLinkType = ParentLocal.peekHeadLinkType()) {
1018+
if (taskLocalHeadLinkType ==
1019+
swift::TaskLocal::NextLinkType::IsNextCreatedInTaskGroupBody) {
1020+
swift_task_localsCopyToTaskGroupChildTaskDefensively(task);
1021+
taskLocalStorageInitialized = true;
1022+
}
1023+
}
1024+
}
1025+
1026+
if (!taskLocalStorageInitialized) {
1027+
// just initialize the storage normally
1028+
task->_private().Local.initializeLinkParent(task, parent);
1029+
}
9921030
}
9931031

9941032
// Configure the initial context.
@@ -1050,7 +1088,7 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
10501088
#endif
10511089
swift_retain(task);
10521090
task->flagAsAndEnqueueOnExecutor(
1053-
serialExecutor); // FIXME: pass the task executor explicitly?
1091+
serialExecutor);
10541092
}
10551093

10561094
return {task, initialContext};

0 commit comments

Comments
 (0)