Skip to content

Swift isolation bug lets through race condition #84376

@waldemarhorwat

Description

@waldemarhorwat

Description

The following program compiles and runs using the Xcode Swift 6.2 compiler in Swift 6 mode with no warnings or errors:

import Foundation

class Stateful {
  var x: Int

  init() { x = 0 }
}

class Entry {
  let id: Int
  var s: Stateful

  init(_ id: Int, s: Stateful) {
    self.id = id
    self.s = s
  }

  func asyncWork() async {
    s.x += 1
    print("Task \(id): x=\(s.x)")
    for _ in 1...500 { }
    print("Task \(id): x=\(s.x)")
  }
}

@MainActor class MyClass {
  var entries: [Entry] = []

  func tenTasks() async {
    let s = Stateful()
    entries = (0...9).map { Entry($0, s: s) }
    let totalCount = entries.count
    await withTaskGroup(of: Void.self) { group in
      var i = 0
      while i < totalCount {
        let entry = entries[i]
        group.addTask { await entry.asyncWork() }
        i += 1
      }
      for await _ in group { }
    }
  }
}

print("Begin")
await MyClass().tenTasks()
print("End")

Yet it has an obvious race condition on x, with its value changing from the point of view of a task without the task yielding or modifying it. Here’s one output:

Begin
Task 0: x=1
Task 1: x=2
Task 2: x=3
Task 3: x=4
Task 0: x=4
Task 5: x=5
Task 4: x=6
Task 6: x=7
Task 7: x=8
Task 1: x=8
Task 9: x=9
Task 8: x=10
Task 2: x=10
Task 5: x=10
Task 3: x=10
Task 6: x=10
Task 4: x=10
Task 7: x=10
Task 9: x=10
Task 8: x=10
End


Were I to change the line

entries = (0...9).map { Entry($0, s: s) }

to

let entries = (0...9).map { Entry($0, s: s) }

then the compiler would correctly diagnose the problem:

Passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure

but the program as originally listed races without generating any errors or warnings. Is this intentional?

I’m running Swift 6.2 on the current release Xcode 26 with language set to Swift 6 and “nonisolated(nonsending) By Default” set to YES, although the bug also appears in older versions of Swift.

Reproduction

import Foundation

class Stateful {
  var x: Int

  init() { x = 0 }
}

class Entry {
  let id: Int
  var s: Stateful

  init(_ id: Int, s: Stateful) {
    self.id = id
    self.s = s
  }

  func asyncWork() async {
    s.x += 1
    print("Task \(id): x=\(s.x)")
    for _ in 1...500 { }
    print("Task \(id): x=\(s.x)")
  }
}

@MainActor class MyClass {
  var entries: [Entry] = []

  func tenTasks() async {
    let s = Stateful()
    entries = (0...9).map { Entry($0, s: s) }
    let totalCount = entries.count
    await withTaskGroup(of: Void.self) { group in
      var i = 0
      while i < totalCount {
        let entry = entries[i]
        group.addTask { await entry.asyncWork() }
        i += 1
      }
      for await _ in group { }
    }
  }
}

print("Begin")
await MyClass().tenTasks()
print("End")

Expected behavior

Compiler in Swift 6 mode rejects the code due to the race condition

Environment

Swift 6.2 in Xcode 26 release, but also happens in earlier Swift versions.

Additional information

See discussion on Swift forum.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.triage 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