// RUN: %empty-directory(%t) // RUN: %target-build-swift %s -target %target-swift-5.1-abi-triple -o %t/voucher_propagation // RUN: %target-codesign %t/voucher_propagation // RUN: MallocStackLogging=1 %target-run %t/voucher_propagation // REQUIRES: executable_test // REQUIRES: concurrency // Use objc_interop as a proxy for voucher support in the OS. // REQUIRES: objc_interop // REQUIRES: concurrency_runtime // UNSUPPORTED: back_deployment_runtime // REQUIRES: OS=macosx import Darwin import Dispatch // expected-warning {{add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'Dispatch'}} import StdlibUnittest // These are an attempt to simulate some kind of async work, and the // computations being performed are not intended to actually make any kind of // sense. actor Accumulator { let label: String var x = 0 init(label: String) { self.label = label } func add(_ n: Int, _ voucher: voucher_t?) async { let currentVoucher = voucher_copy() expectEqual(voucher, currentVoucher) print("\(n) \(label)", voucher: currentVoucher) os_release(currentVoucher) x += n usleep(1000) } func add(sequence: T, _ voucher: voucher_t?) async -> Int where T.Element == Int { for n in sequence { await add(n, voucher) } return x } func get() -> Int { x } } actor Combiner { var accumulators: [Accumulator] = [] func add(accumulator: Accumulator) { accumulators.append(accumulator) } func sum() async -> Int { var sum = 0 for a in accumulators { fputs("Accumulating from \(ptrstr(a))\n", stderr) sum += await a.get() } return sum } } actor Counter { var n = 0 func increment() { n += 1 } func get() -> Int { n } } @available(SwiftStdlib 6.1, *) actor ActorWithSelfIsolatedDeinit { let expectedVoucher: voucher_t? let group: DispatchGroup init(expectedVoucher: voucher_t?, group: DispatchGroup) { self.expectedVoucher = expectedVoucher self.group = group } isolated deinit { expectTrue(isCurrentExecutor(self.unownedExecutor)) let currentVoucher = voucher_copy() expectEqual(expectedVoucher, currentVoucher) os_release(currentVoucher) group.leave() } } @globalActor actor AnotherActor: GlobalActor { static let shared = AnotherActor() func performTesting(_ work: @Sendable () -> Void) { work() } } @available(SwiftStdlib 6.1, *) actor ActorWithDeinitIsolatedOnAnother { let expectedVoucher: voucher_t? let group: DispatchGroup init(expectedVoucher: voucher_t?, group: DispatchGroup) { self.expectedVoucher = expectedVoucher self.group = group } @AnotherActor deinit { expectTrue(isCurrentExecutor(AnotherActor.shared.unownedExecutor)) let currentVoucher = voucher_copy() expectEqual(expectedVoucher, currentVoucher) os_release(currentVoucher) group.leave() } } @available(SwiftStdlib 6.1, *) class ClassWithIsolatedDeinit { let expectedVoucher: voucher_t? let group: DispatchGroup init(expectedVoucher: voucher_t?, group: DispatchGroup) { self.expectedVoucher = expectedVoucher self.group = group } @AnotherActor deinit { expectTrue(isCurrentExecutor(AnotherActor.shared.unownedExecutor)) let currentVoucher = voucher_copy() expectEqual(expectedVoucher, currentVoucher) os_release(currentVoucher) group.leave() } } // Make a nice string for a pointer, like what %p would produce in printf. func ptrstr(_ ptr: T) -> String { "0x" + String(unsafeBitCast(ptr, to: UInt.self), radix: 16) } // Print a voucher to stderr with an optional label. func print(_ s: String = "", voucher: voucher_t?) { fputs("\(s) - \(ptrstr(voucher))\n", stderr) } // Look up a symbol using dlsym and cast the result to an arbitrary type. func lookup(_ name: String) -> T { let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2) let result = dlsym(RTLD_DEFAULT, name) return unsafeBitCast(result, to: T.self) } // We'll use os_activity calls to make our test vouchers. The calls aren't in // the headers so we need to look them up dynamically. let _os_activity_create = lookup("_os_activity_create") as @convention(c) (UnsafeRawPointer, UnsafePointer, UnsafeRawPointer, UInt32) -> voucher_t? let OS_ACTIVITY_NONE = lookup("_os_activity_none") as UnsafeRawPointer let OS_ACTIVITY_FLAG_DETACHED = 1 as UInt32 // Look up the voucher calls we'll be using. Vouchers are ObjC objects, but we // want total control over their memory management, so we'll treat them as raw // pointers instead, and manually manage their memory. typealias voucher_t = UnsafeMutableRawPointer let voucher_copy = lookup("voucher_copy") as @convention(c) () -> voucher_t? let voucher_adopt = lookup("voucher_adopt") as @convention(c) (voucher_t?) -> voucher_t? let os_retain = lookup("os_retain") as @convention(c) (voucher_t?) -> voucher_t? let os_release = lookup("os_release") as @convention(c) (voucher_t?) -> Void let isCurrentExecutor = lookup("swift_task_isCurrentExecutor") as @convention(thin) (UnownedSerialExecutor) -> Bool // Run some async code with test vouchers. Wait for the async code to complete, // then verify that the vouchers aren't leaked. func withVouchers(call: @Sendable @escaping (voucher_t?, voucher_t?, voucher_t?) async -> Void) { // We'll store weak pointers to the vouchers to verify that they don't leak. // If the voucher was deallocated after we're done, then the weak references // will be nil. We can't use Swift `weak` references since we're treating // vouchers as raw pointers. We'll use the ObjC runtime APIs directly. Those // require a stable address for the storage, so we'll allocate that manually. let weakPtrsAllocation = UnsafeMutablePointer.allocate(capacity: 3) weakPtrsAllocation.initialize(repeating: nil, count: 3) let weakPtrs = AutoreleasingUnsafeMutablePointer(weakPtrsAllocation) // Helper function to store three vouchers into the weak storage. func storeWeak(vouchers: [voucher_t?]) { for (i, voucher) in vouchers.enumerated() { objc_storeWeak(weakPtrs + i, unsafeBitCast(voucher, to: AnyObject?.self)) } } // Make convenient pointers to the individual weak variables. let weak1 = AutoreleasingUnsafeMutablePointer(weakPtrs + 0) let weak2 = AutoreleasingUnsafeMutablePointer(weakPtrs + 1) let weak3 = AutoreleasingUnsafeMutablePointer(weakPtrs + 2) do { let v1 = _os_activity_create(#dsohandle, "Swift Test Voucher 1", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DETACHED) let v2 = _os_activity_create(#dsohandle, "Swift Test Voucher 2", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DETACHED) let v3 = _os_activity_create(#dsohandle, "Swift Test Voucher 3", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DETACHED) print("Created v1", voucher: v1) print("Created v2", voucher: v2) print("Created v3", voucher: v3) // Place the vouchers in the weak variables and verify that it worked. storeWeak(vouchers: [v1, v2, v3]) autoreleasepool { expectNotNil(objc_loadWeak(weak1)) expectNotNil(objc_loadWeak(weak2)) expectNotNil(objc_loadWeak(weak3)) } // Start the async call in the background, waiting for it to complete. let group = DispatchGroup() group.enter() Task { await call(v1, v2, v3) // Clear any voucher that the call adopted. adopt(voucher: nil) group.leave() // expected-complete-tns-warning {{capture of 'group' with non-Sendable type 'DispatchGroup' in a '@Sendable' closure}} } group.wait() // Release what should be the last reference to the vouchers. os_release(v1) os_release(v2) os_release(v3) } func now() -> UInt64 { return clock_gettime_nsec_np(CLOCK_UPTIME_RAW) } // The vouchers may take a moment to be destroyed, as background threads // finish what they're doing. Wait for the voucher weak references to become // nil. We'll give them ten seconds, and otherwise assume they've leaked. let start = now(); var allNil = false; while !allNil && now() - start < 10_000_000_000 { autoreleasepool { allNil = objc_loadWeak(weak1) == nil && objc_loadWeak(weak2) == nil && objc_loadWeak(weak3) == nil } } // If any weak reference contains non-nil at this point, a voucher has leaked. expectNil(objc_loadWeak(weak1)) expectNil(objc_loadWeak(weak2)) expectNil(objc_loadWeak(weak3)) // Clean up the weak references. storeWeak(vouchers: [nil, nil, nil]) weakPtrsAllocation.deallocate() } // Adopt the given voucher on the current thread. This takes the voucher at +0 // and handles the memory management around voucher_adopt. func adopt(voucher: voucher_t?) { os_release(voucher_adopt(os_retain(voucher))) } let tests = TestSuite("Voucher Propagation") if #available(SwiftStdlib 5.1, *) { tests.test("simple voucher propagation") { withVouchers { v1, v2, v3 in let a = Accumulator(label: "a: ") adopt(voucher: v1) await a.add(42, v1) } } tests.test("voucher propagation with a simple async let") { withVouchers { v1, v2, v3 in let a1 = Accumulator(label: "a1: ") let a2 = Accumulator(label: "a2: ") adopt(voucher: v1) async let r1: () = a1.add(42, v1) adopt(voucher: v2) async let r2: () = a2.add(42, v2) _ = await (r1, r2) } } tests.test("Task {} voucher propagation") { withVouchers { v1, v2, v3 in let a = Accumulator(label: "a: ") adopt(voucher: v1) let group = DispatchGroup() group.enter() Task { await a.add(42, v1) group.leave() } group.wait() } } tests.test("Task.detached {} voucher propagation") { withVouchers { v1, v2, v3 in let a = Accumulator(label: "a: ") adopt(voucher: v1) let group = DispatchGroup() group.enter() Task.detached { // Task.detached should NOT propagate vouchers, so tell add to look for // nil. await a.add(42, nil) group.leave() } group.wait() } } tests.test("complex voucher propagation") { withVouchers { v1, v2, v3 in fputs("Hello, whirled.\n", stderr) let a1 = Accumulator(label: "a1: ") let a2 = Accumulator(label: "a2: ") adopt(voucher: v1) @Sendable func go(_ log: String, _ v: voucher_t?, _ a: Accumulator, _ n: Int) async -> Int { print("Starting \(log): ", voucher: v) return await a.add(sequence: (n*10)..<(n*10+10), v) } adopt(voucher: v1) async let r1 = go("1 a1", v1, a1, 1) adopt(voucher: v2) async let r2 = go("2 a2", v2, a2, 2) async let r3 = go("3 a1", v2, a1, 3) adopt(voucher: v1) async let r4 = go("4 a2", v1, a2, 4) adopt(voucher: v1) async let r5 = go("5 a1", v1, a1, 5) async let r6 = go("6 a2", v1, a2, 6) adopt(voucher: v2) async let r7 = go("7 a1", v2, a1, 7) async let r8 = go("8 a2", v2, a2, 8) adopt(voucher: v3) async let r9 = go("9 a1", v3, a1, 9) async let r10 = go("10 a2", v3, a2, 10) fputs("async let results: \((await r1, await r2, await r3, await r4, await r5, await r6, await r7, await r8, await r9, await r10))\n", stderr) fputs("\(await a2.get())\n", stderr) let combiner = Combiner() async let add1: () = await combiner.add(accumulator: a1) async let add2: () = await combiner.add(accumulator: a2) _ = await (add1, add2) fputs("combiner 1 - \(await combiner.sum())\n", stderr) async let add3: () = await combiner.add(accumulator: a1) async let add4: () = await combiner.add(accumulator: a2) _ = await (add3, add4) fputs("combiner 2 - \(await combiner.sum())\n", stderr) for n in 0..<10 { async let a = combiner.sum() async let b = combiner.sum() fputs("combiner loop \(n) - \(await a + b)\n", stderr) } } } tests.test("voucher propagation with mixed concurrency/dispatch code") { withVouchers {v1, v2, v3 in let a = Accumulator(label: "a: ") let group = DispatchGroup() let n = Counter() let limit = 100 group.enter() @Sendable func detachedTask() async { let voucher = [v1, v2, v3][await n.get() % 3] adopt(voucher: voucher) let currentVoucher = voucher_copy() expectEqual(voucher, currentVoucher) os_release(currentVoucher) DispatchQueue.global().async { let currentVoucher = voucher_copy() expectEqual(voucher, currentVoucher) os_release(currentVoucher) Task { let currentVoucher = voucher_copy() expectEqual(voucher, currentVoucher) os_release(currentVoucher) async let g: () = withTaskGroup(of: Void.self, body: { group in for _ in 0..<10 { group.async { let currentVoucher = voucher_copy() expectEqual(voucher, currentVoucher) os_release(currentVoucher) } } }) async let add: () = a.add(42, voucher) _ = await (g, add) if await n.get() >= limit { group.leave() // expected-warning 2{{capture of 'group' with non-Sendable type 'DispatchGroup' in a '@Sendable' closure}} } else { await n.increment() await detachedTask() } } } } await detachedTask() group.wait() } } if #available(SwiftStdlib 6.1, *) { tests.test("voucher propagation in isolated deinit [fast path]") { withVouchers { v1, v2, v3 in let group = DispatchGroup() group.enter() group.enter() group.enter() Task { await AnotherActor.shared.performTesting { adopt(voucher: v1) _ = ClassWithIsolatedDeinit(expectedVoucher: v1, group: group) } await AnotherActor.shared.performTesting { adopt(voucher: v2) _ = ActorWithSelfIsolatedDeinit(expectedVoucher: v2, group: group) } await AnotherActor.shared.performTesting { adopt(voucher: v3) _ = ActorWithDeinitIsolatedOnAnother(expectedVoucher: v3, group: group) } } group.wait() } } tests.test("voucher propagation in isolated deinit [slow path]") { withVouchers { v1, v2, v3 in let group = DispatchGroup() group.enter() group.enter() Task { do { adopt(voucher: v1) _ = ActorWithDeinitIsolatedOnAnother(expectedVoucher: v1, group: group) } do { adopt(voucher: v2) _ = ClassWithIsolatedDeinit(expectedVoucher: v2, group: group) } } group.wait() } } } } runAllTests()