swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.18k stars 10.32k forks source link

Swift 6: runtime crash when using a "sending" closure parameter #74457

Open groue opened 2 months ago

groue commented 2 months ago

Description

Hello,

A Swift program that wraps a "sending" closure parameter in a struct crashes at runtime EXC_BAD_ACCESS (code=1, address=0x0).

Programs do this as a workaround for the warnings/errors described at #73315. They were were eventually identified as unavoidable by @rjmccall in this forum post. We thus need to instruct the compiler that the program is valid through the mean of unchecked constructs, such as wrapping the sending closure in an unchecked Sendable wrapper that can cross isolation domains.

As an illustration of the problem, please find attached a small package that adds the following method to DispatchQueue.

Note: this code is an illustration for this GitHub issue, not real code. Now my real code indeed publishes public apis that accept sending closures and internally use DispatchQueue, because I have to, and that's how I found this crash).

extension DispatchQueue {
    /// Returns the result of the provided closure after it has been
    /// executed in the dispatch queue.
    func execute<T>(_ work: sending @escaping () -> sending T) async -> sending T
}

This method compiles fine, but crashes at runtime.

Reproduction

Full package: SwiftIssue.zip

Package.swift

// swift-tools-version: 5.10
import PackageDescription

let package = Package(
    name: "SwiftIssue",
    platforms: [
        .macOS(.v10_15),
    ],
    products: [
        .library(name: "SwiftIssue", targets: ["SwiftIssue"]),
    ],
    targets: [
        .target(
            name: "SwiftIssue",
            swiftSettings: [
                .enableUpcomingFeature("StrictConcurrency"),
                .enableExperimentalFeature("RegionBasedIsolation"),
                .enableExperimentalFeature("TransferringArgsAndResults"),
            ]
        ),
        .testTarget(
            name: "SwiftIssueTests",
            dependencies: ["SwiftIssue"]
        )
    ]
)

SwiftIssue.swift

import Dispatch

/// Helps compiler accept valid code it can't validate.
struct UncheckedSendableWrapper<Value>: @unchecked Sendable {
    let value: Value
}

extension DispatchQueue {
    /// Returns the result of the provided closure after it has been
    /// executed in the dispatch queue.
    public func execute<T>(_ work: sending @escaping () -> sending T) async -> sending T {
        // Avoid a compiler warning:
        // > Capture of 'work' with non-sendable type '() -> sending T' in
        // > a `@Sendable` closure
        let work = UncheckedSendableWrapper(value: work)
        return await withUnsafeContinuation { continuation in
            self.async {
                continuation.resume(returning: work.value())
            }
        }
    }
}

SwiftIssueTests.swift

import Dispatch
import SwiftIssue
import XCTest

final class SwiftIssueTests: XCTestCase {
    // 💥 EXC_BAD_ACCESS (code=1, address=0x0)
    func testSwiftIssue() async {
        let value = await DispatchQueue.main.execute { 1 }
        XCTAssertEqual(value, 1)
    }
}

Stack dump

* thread #4, queue = 'com.apple.root.user-initiated-qos.cooperative', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000
  * frame #1: 0x00000001001ae2a0 SwiftIssueTests`OS_dispatch_queue.execute<Int>() at SwiftIssue.swift:15:20
    frame #2: 0x00000001001acd48 SwiftIssueTests`SwiftIssueTests.testSwiftIssue() at SwiftIssueTests.swift:7
    frame #3: 0x00000001001ad140 SwiftIssueTests`@objc closure #1 in SwiftIssueTests.testSwiftIssue() at <compiler-generated>:0
    frame #4: 0x00000001001ad2b0 SwiftIssueTests`partial apply for @objc closure #1 in SwiftIssueTests.testSwiftIssue() at <compiler-generated>:0
    frame #5: 0x00000001001ad838 SwiftIssueTests`thunk for @escaping @callee_guaranteed @Sendable @async () -> () at <compiler-generated>:0
    frame #6: 0x00000001001ad994 SwiftIssueTests`partial apply for thunk for @escaping @callee_guaranteed @Sendable @async () -> () at <compiler-generated>:0
    frame #7: 0x00000001001ada70 SwiftIssueTests`thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> () at <compiler-generated>:0
    frame #8: 0x00000001001adbd4 SwiftIssueTests`partial apply for thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> () at <compiler-generated>:0
    frame #9: 0x00000001001adf48 SwiftIssueTests`specialized thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> (@out A) at <compiler-generated>:0
    frame #10: 0x00000001001ae090 SwiftIssueTests`partial apply for specialized thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> (@out A) at <compiler-generated>:0

Expected behavior

No crash

Environment

swift-driver version: 1.109.2 Apple Swift version 6.0 (swiftlang-6.0.0.3.300 clang-1600.0.20.10) Target: arm64-apple-macosx14.0

Additional information

The program crashes in both Swift 5 and Swift 6 language modes.

I know a workaround:

 extension DispatchQueue {
     /// Returns the result of the provided closure after it has been
     /// executed in the dispatch queue.
     public func execute<T>(_ work: sending @escaping () -> sending T) async -> sending T {
         // Avoid a compiler warning:
         // > Task-isolated value of type '() -> Void' passed as a strongly
         // > transferred parameter; later accesses could race.
-        let work = UncheckedSendableWrapper(value: work)
+        let workaround: () -> T = work
+        let work = UncheckedSendableWrapper(value: workaround)
         return await withUnsafeContinuation { continuation in
             sendingAsync {
                 continuation.resume(returning: work.value())
             }
         }
     }
 }
groue commented 2 months ago

The trunk and swift 6 snapshots crash on compilation, not at runtime: #74458

xedin commented 2 months ago

cc @gottesmm

groue commented 1 month ago

Bug still present in Xcode 16.0 beta 3 (16A5202i)

% swiftc -v
Apple Swift version 6.0 (swiftlang-6.0.0.5.15 clang-1600.0.22.6)
Target: arm64-apple-macosx14.0
groue commented 3 weeks ago

Bug still present in Xcode 16.0 beta 4