Skip to content

Commit e3dff64

Browse files
authored
Merge pull request swiftlang#77325 from kubamracek/embedded-concurrency-fix-leaks
[Concurrency] Fix memory leak around mismatched refcounting in Embedded Concurrency
2 parents 037d910 + d78be48 commit e3dff64

File tree

8 files changed

+140
-19
lines changed

8 files changed

+140
-19
lines changed

include/swift/ABI/Task.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class ContinuationAsyncContext;
4848
// _swift_concurrency_debug_internal_layout_version and add a comment describing
4949
// the new version.
5050

51-
extern FullMetadata<DispatchClassMetadata> jobHeapMetadata;
51+
extern const HeapMetadata *jobHeapMetadataPtr;
52+
extern const HeapMetadata *taskHeapMetadataPtr;
5253

5354
/// A schedulable job.
5455
class alignas(2 * alignof(void*)) Job :
@@ -106,14 +107,14 @@ class alignas(2 * alignof(void*)) Job :
106107
};
107108

108109
Job(JobFlags flags, JobInvokeFunction *invoke,
109-
const HeapMetadata *metadata = &jobHeapMetadata)
110+
const HeapMetadata *metadata = jobHeapMetadataPtr)
110111
: HeapObject(metadata), Flags(flags), RunJob(invoke) {
111112
Voucher = voucher_copy();
112113
assert(!isAsyncTask() && "wrong constructor for a task");
113114
}
114115

115116
Job(JobFlags flags, TaskContinuationFunction *invoke,
116-
const HeapMetadata *metadata = &jobHeapMetadata,
117+
const HeapMetadata *metadata = jobHeapMetadataPtr,
117118
bool captureCurrentVoucher = true)
118119
: HeapObject(metadata), Flags(flags), ResumeTask(invoke) {
119120
if (captureCurrentVoucher)

stdlib/public/Concurrency/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ if(SWIFT_SHOULD_BUILD_EMBEDDED_STDLIB AND SWIFT_SHOULD_BUILD_EMBEDDED_CONCURRENC
273273
-Xfrontend -emit-empty-object-file
274274
${SWIFT_RUNTIME_CONCURRENCY_SWIFT_FLAGS}
275275
C_COMPILE_FLAGS
276-
${extra_c_compile_flags} ${SWIFT_RUNTIME_CONCURRENCY_C_FLAGS} -DSWIFT_CONCURRENCY_EMBEDDED=1
276+
${extra_c_compile_flags} ${SWIFT_RUNTIME_CONCURRENCY_C_FLAGS} -DSWIFT_CONCURRENCY_EMBEDDED=1 -DSWIFT_RUNTIME_EMBEDDED=1
277277
MODULE_DIR "${CMAKE_BINARY_DIR}/lib/swift/embedded"
278278
SDK "embedded"
279279
ARCHITECTURE "${mod}"

stdlib/public/Concurrency/ExecutorChecks.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ static_assert((SwiftJobPriority)swift::JobPriority::Unspecified
5454
// Job (has additional fields not exposed via SwiftJob)
5555
static_assert(sizeof(swift::Job) >= sizeof(SwiftJob));
5656
static_assert(offsetof(swift::Job, metadata) == offsetof(SwiftJob, metadata));
57+
#if !SWIFT_CONCURRENCY_EMBEDDED
5758
static_assert(offsetof(swift::Job, refCounts) == offsetof(SwiftJob, refCounts));
59+
#else
60+
static_assert(offsetof(swift::Job, embeddedRefcount) == offsetof(SwiftJob, refCounts));
61+
#endif
5862
static_assert(offsetof(swift::Job, SchedulerPrivate) == offsetof(SwiftJob, schedulerPrivate));
5963
static_assert(offsetof(swift::Job, SchedulerPrivate[0]) == offsetof(SwiftJob, schedulerPrivate[0]));
6064
static_assert(offsetof(swift::Job, SchedulerPrivate[1]) == offsetof(SwiftJob, schedulerPrivate[1]));

stdlib/public/Concurrency/Task.cpp

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ InitialTaskExecutorOwnedPreferenceTaskOptionRecord::getExecutorRefFromUnownedTas
215215
return executorRef;
216216
}
217217

218-
219218
void NullaryContinuationJob::process(Job *_job) {
220219
auto *job = cast<NullaryContinuationJob>(_job);
221220

@@ -393,7 +392,9 @@ static void jobInvoke(void *obj, void *unused, uint32_t flags) {
393392
// Magic constant to identify Swift Job vtables to Dispatch.
394393
static const unsigned long dispatchSwiftObjectType = 1;
395394

396-
FullMetadata<DispatchClassMetadata> swift::jobHeapMetadata = {
395+
#if !SWIFT_CONCURRENCY_EMBEDDED
396+
397+
static FullMetadata<DispatchClassMetadata> jobHeapMetadata = {
397398
{
398399
{
399400
/*type layout*/ nullptr,
@@ -432,11 +433,47 @@ static FullMetadata<DispatchClassMetadata> taskHeapMetadata = {
432433
}
433434
};
434435

436+
435437
const void *const swift::_swift_concurrency_debug_jobMetadata =
436438
static_cast<Metadata *>(&jobHeapMetadata);
437439
const void *const swift::_swift_concurrency_debug_asyncTaskMetadata =
438440
static_cast<Metadata *>(&taskHeapMetadata);
439441

442+
const HeapMetadata *swift::jobHeapMetadataPtr =
443+
static_cast<HeapMetadata *>(&jobHeapMetadata);
444+
const HeapMetadata *swift::taskHeapMetadataPtr =
445+
static_cast<HeapMetadata *>(&taskHeapMetadata);
446+
447+
#else // SWIFT_CONCURRENCY_EMBEDDED
448+
449+
// This matches the embedded class metadata layout in IRGen and in
450+
// EmbeddedRuntime.swift.
451+
typedef struct EmbeddedClassMetadata {
452+
void *superclass;
453+
HeapObjectDestroyer *__ptrauth_swift_heap_object_destructor destroy;
454+
void *ivar_destroyer;
455+
} EmbeddedHeapObject;
456+
457+
static EmbeddedClassMetadata jobHeapMetadata = {
458+
0, &destroyJob, 0,
459+
};
460+
461+
static EmbeddedClassMetadata taskHeapMetadata = {
462+
0, &destroyTask, 0,
463+
};
464+
465+
const void *const swift::_swift_concurrency_debug_jobMetadata =
466+
(Metadata *)(&jobHeapMetadata);
467+
const void *const swift::_swift_concurrency_debug_asyncTaskMetadata =
468+
(Metadata *)(&taskHeapMetadata);
469+
470+
const HeapMetadata *swift::jobHeapMetadataPtr =
471+
(HeapMetadata *)(&jobHeapMetadata);
472+
const HeapMetadata *swift::taskHeapMetadataPtr =
473+
(HeapMetadata *)(&taskHeapMetadata);
474+
475+
#endif
476+
440477
static void completeTaskImpl(AsyncTask *task,
441478
AsyncContext *context,
442479
SwiftError *error) {
@@ -959,14 +996,12 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
959996
if (asyncLet) {
960997
// Initialize the refcount bits to "immortal", so that
961998
// ARC operations don't have any effect on the task.
962-
task = new(allocation) AsyncTask(&taskHeapMetadata,
963-
InlineRefCounts::Immortal, jobFlags,
964-
function, initialContext,
965-
captureCurrentVoucher);
999+
task = new (allocation)
1000+
AsyncTask(taskHeapMetadataPtr, InlineRefCounts::Immortal, jobFlags,
1001+
function, initialContext, captureCurrentVoucher);
9661002
} else {
967-
task = new(allocation) AsyncTask(&taskHeapMetadata, jobFlags,
968-
function, initialContext,
969-
captureCurrentVoucher);
1003+
task = new (allocation) AsyncTask(taskHeapMetadataPtr, jobFlags, function,
1004+
initialContext, captureCurrentVoucher);
9701005
}
9711006

9721007
// Initialize the child fragment if applicable.

stdlib/public/SwiftShims/swift/shims/HeapObject.h

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ struct HeapObject {
5353
/// This is always a valid pointer to a metadata object.
5454
HeapMetadata const *__ptrauth_objc_isa_pointer metadata;
5555

56+
#if !SWIFT_RUNTIME_EMBEDDED
57+
5658
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
5759

5860
#ifndef __swift__
@@ -70,11 +72,40 @@ struct HeapObject {
7072
: metadata(newMetadata)
7173
, refCounts(InlineRefCounts::Immortal)
7274
{ }
75+
#endif // __swift__
76+
77+
#else // SWIFT_RUNTIME_EMBEDDED
78+
uintptr_t embeddedRefcount;
79+
80+
// Note: The immortal refcount value is also hard-coded in IRGen in
81+
// `irgen::emitConstantObject`, and in EmbeddedRuntime.swift.
82+
#if __POINTER_WIDTH__ == 64
83+
static const uintptr_t EmbeddedImmortalRefCount = 0x7fffffffffffffffull;
84+
#elif __POINTER_WIDTH__ == 32
85+
static const uintptr_t EmbeddedImmortalRefCount = 0x7fffffff;
86+
#elif __POINTER_WIDTH__ == 16
87+
static const uintptr_t EmbeddedImmortalRefCount = 0x7fff;
88+
#endif
7389

90+
#ifndef __swift__
91+
HeapObject() = default;
92+
93+
// Initialize a HeapObject header as appropriate for a newly-allocated object.
94+
constexpr HeapObject(HeapMetadata const *newMetadata)
95+
: metadata(newMetadata), embeddedRefcount(1) {}
96+
97+
// Initialize a HeapObject header for an immortal object
98+
constexpr HeapObject(HeapMetadata const *newMetadata,
99+
InlineRefCounts::Immortal_t immortal)
100+
: metadata(newMetadata), embeddedRefcount(EmbeddedImmortalRefCount) {}
101+
#endif // __swift__
102+
103+
#endif // SWIFT_RUNTIME_EMBEDDED
104+
105+
#ifndef __swift__
74106
#ifndef NDEBUG
75107
void dump() const SWIFT_USED;
76108
#endif
77-
78109
#endif // __swift__
79110
};
80111

stdlib/public/core/EmbeddedRuntime.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public struct HeapObject {
9999
// to think about supporting (or banning) weak and/or unowned references.
100100
var refcount: Int
101101

102-
// Note: The immortalRefCount value is also hard-coded in IRGen in `irgen::emitConstantObject`.
102+
// Note: The immortalRefCount value is also hard-coded in IRGen in `irgen::emitConstantObject`, and in HeapObject.h.
103103
#if _pointerBitWidth(_64)
104104
static let doNotFreeBit = Int(bitPattern: 0x8000_0000_0000_0000)
105105
static let refcountMask = Int(bitPattern: 0x7fff_ffff_ffff_ffff)

test/embedded/Inputs/debug-malloc.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
#include <stdlib.h>
22
#include <stdio.h>
33

4-
#define HEAP_SIZE (8 * 1024)
4+
#define HEAP_SIZE (128 * 1024)
55

66
__attribute__((aligned(16)))
77
char heap[HEAP_SIZE] = {};
88

99
size_t next_heap_index = 0;
1010

1111
void *calloc(size_t count, size_t size) {
12-
printf("malloc(%ld)", count);
12+
size_t total_size = count * size;
13+
printf("malloc(%ld)", total_size);
1314

14-
if (next_heap_index + count * size > HEAP_SIZE) {
15+
if (total_size % 16 != 0)
16+
total_size = total_size + (16 - total_size % 16);
17+
18+
if (next_heap_index + total_size > HEAP_SIZE) {
1519
puts("HEAP EXHAUSTED\n");
1620
__builtin_trap();
1721
}
1822
void *p = &heap[next_heap_index];
19-
next_heap_index += count * size;
23+
next_heap_index += total_size;
2024
printf("-> %p\n", p);
2125
return p;
2226
}

test/embedded/concurrency-leaks.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend -enable-experimental-feature Embedded -parse-as-library %s -c -o %t/a.o -g -O
3+
// RUN: %target-clang -x c -std=c11 -c %S/Inputs/debug-malloc.c -o %t/debug-malloc.o -g
4+
// RUN: %target-clang %t/a.o %t/debug-malloc.o -o %t/a.out -L%swift_obj_root/lib/swift/embedded/%target-cpu-apple-macos -lswift_Concurrency -lswift_ConcurrencyDefaultExecutor -dead_strip -g
5+
// RUN: %target-run %t/a.out | %FileCheck %s
6+
7+
// REQUIRES: executable_test
8+
// REQUIRES: swift_in_compiler
9+
// REQUIRES: optimized_stdlib
10+
// REQUIRES: OS=macosx
11+
12+
import _Concurrency
13+
14+
@main
15+
struct Main {
16+
static func main() async {
17+
print("start")
18+
// CHECK: start
19+
do {
20+
let x = Task {
21+
return 42
22+
}
23+
_ = await x.value
24+
}
25+
// There should be exactly 5 allocations involved:
26+
// - 2x 32 bytes ... closure context for swift_task_create, closure object to pass to Task.init
27+
// - 1x 320 bytes ... malloc(amountToAllocate) in swift_task_create_common for the Task heap object itself
28+
// - 1x 1016 bytes ... the initial StackAllocator slab in the task-specific allocator
29+
// - 1x 40 bytes ... task status record
30+
// Check that they are all accounted for and free'd.
31+
32+
// CHECK: malloc({{[0-9]+}})-> [[M1:0x[0-9a-f]+]]
33+
// CHECK: malloc({{[0-9]+}})-> [[M2:0x[0-9a-f]+]]
34+
// CHECK: malloc({{[0-9]+}})-> [[M3:0x[0-9a-f]+]]
35+
// CHECK: malloc({{[0-9]+}})-> [[M4:0x[0-9a-f]+]]
36+
// CHECK: free([[M1]])
37+
// CHECK: free([[M2]])
38+
// CHECK: malloc({{[0-9]+}})-> [[M5:0x[0-9a-f]+]]
39+
// CHECK: free([[M5]])
40+
// CHECK: free([[M4]])
41+
// CHECK: free([[M3]])
42+
43+
print("done")
44+
// CHECK: done
45+
}
46+
}

0 commit comments

Comments
 (0)