Skip to content

Combine sink does not respect @MainActor isolation #83339

@AvdLee

Description

@AvdLee

Description

I found a consistent crash due to the Swift 6 strict concurrency checker lacking to detect sink usage in a @MainActor isolated context.

Image Image

Reproduction

Consider the following code:

@MainActor
final class NotificationObserver {
    
    private(set) var didCallNotification = false
    private var cancellables: [AnyCancellable] = []
    
    init() {
        /// 💥 Observing notifications using a selector results in the same crash:
        NotificationCenter.default.addObserver(self, selector: #selector(selectorNotificationCalled), name: .notificationA, object: nil)
        
        /// 💥 Observing notifications using Combine publisher results in a crash:
        /// EXC_BREAKPOINT
        NotificationCenter.default.publisher(for: .notificationA)
            .sink { [weak self] _ in

                self?.didReceiveConcurrentNotification()
            }.store(in: &cancellables)
    }
    
    @objc func selectorNotificationCalled() {
        didReceiveConcurrentNotification()
    }
    
    private func didReceiveConcurrentNotification() {
        didCallNotification = true
    }
}

Posting the notification from a nonisolated or @MainActor isolation domain all works fine. However, calling from a different isolation context will cause a crash:

struct DetachedNotificationPoster {
    func post() async {
        await Task.detached {
            NotificationCenter.default.post(name: .notificationA, object: nil)
        }.value
    }
}

I'm using a detached task here to simplify reproduction.

Here's a test in case that helps:

@MainActor
@Test func detachedTaskPost() async throws {
    let observer = NotificationObserver()
    let poster = DetachedNotificationPoster()
    await poster.post()
    #expect(observer.didCallNotification)
}

Stack dump

0x18e34e55c <+76>:  adrp   x1, 59
    0x18e34e560 <+80>:  add    x1, x1, #0x9e5            ; "%sBlock was %sexpected to execute on queue [%s (%p)]"
    0x18e34e564 <+84>:  sub    x0, x29, #0x18
    0x18e34e568 <+88>:  bl     0x18e3861a8               ; symbol stub for: asprintf
    0x18e34e56c <+92>:  ldur   x19, [x29, #-0x18]
    0x18e34e570 <+96>:  str    x19, [sp]
    0x18e34e574 <+100>: adrp   x0, 59
    0x18e34e578 <+104>: add    x0, x0, #0xa50            ; "%s"
    0x18e34e57c <+108>: bl     0x18e37eb68               ; _dispatch_log
    0x18e34e580 <+112>: adrp   x8, 441525
    0x18e34e584 <+116>: str    x19, [x8, #0x8c0]
->  0x18e34e588 <+120>: brk    #0x1

Expected behavior

I would expect Concurrency Checking to take place, similarly to use e.g. the notification observer with a block:

Image

Environment

swift-driver version: 1.127.10 Apple Swift version 6.2 (swiftlang-6.2.0.14.8 clang-1700.3.14.6)
Target: arm64-apple-macosx15.0

Additional information

This impacts quite some projects that currently migrated to Swift 6 & Strict Concurrency while having Combine publishers. This is an example of notifications, but I would not be surprised if this could also happen to other kind of publishers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.crashBug: A crash, i.e., an abnormal termination of softwaretriage neededThis issue needs more specific labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions