A concurrency safety hole related to global actor isolated methods

The compiler is supposed to reject passing NonSendables across isolation domains. But when dealing with global isolated methods, it often fails.

class A { 
  func a() async { }
  @MainActor 
  func b() async { }

  // call isolated from nonisolated
  func c() async {
    await b()     // <- ❗️❗️❗️ no compiler errors, not as expected
  }

  // call nonisolated from isolated
  @MainActor
  func d() async {
    await a()     // <- compiler emits an error, as expected
  }
}

Both c and d are ill-formed, but the compiler can only detect errors in d.

It is possible to construct an actual thread safe violation leveraging this vulnerability:

class Hell {
    var value = 0

    func a() async {
        value += 100
        await b()
    }

    @MainActor
    func b() async {
        Task {
            for _ in 0..<100 {
                value += 1
            }
        }
    }

    nonisolated func releaseDemon() async {
        for _ in 0..<100 {
            await a()
        }
    }
}

await Hell().releaseDemon()

The compiler has no problem with this code, but if we run it with thread sanitizer enabled, we can clearly see the contention on value.

Edit: I can confirm this is another regression, Swift 5.10 was able to detect it.

6 Likes

I've submitted a issue on the repo: Global actor isolated methods safety hole. · Issue #80295 · swiftlang/swift · GitHub.

2 Likes