Skip to content

Data race involving non-Sendable class may be induced when concurrently modified with task groups and actor isolation control #78360

Open
@slice

Description

@slice

Description

By combining task groups, actor isolation control (isolated (any Actor)?), and a small trampoline function, a data race may be induced at runtime through concurrent access and modification of a Dictionary. Swift isn't supposed to let this code compile. When certain elements of this test case are removed, then an error is correctly emitted.

Reproduction

// xcrun swiftc -v -swift-version 6 -O Issue.swift && ./Issue

import Dispatch

@_optimize(none)
func blackHole(_: some Any) {}

func trampoline(
  _ operation: () async -> Void,
  isolation: isolated (any Actor)? = #isolation
) async {
  await operation()
}

final class State {
  var dict = [Int: Bool]()

  func spawnModifyingTasks() async {
    let absurd = 0...1_000_000
    await withTaskGroup(of: Bool.self) { group in
      for _ in absurd {
        group.addTask { .random() }
      }

      for await bool in group {
        dict[Int.random(in: absurd)] = bool
      }
    }
  }

  func continuouslyModify(isolation: isolated (any Actor)? = #isolation) async {
    await trampoline {
      await spawnModifyingTasks()
    }
  }
}

let state = State()

// concurrent modification
Task {
  await state.continuouslyModify()
}

// concurrent access
Task {
  while true { blackHole(state.dict.count) }
}

dispatchMain()

Stack dump

(lldb) r
Process 54454 launched: '/Users/skip/src/scraps/Contents' (arm64)
Process 54454 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x8000000000000010)
    frame #0: 0x0000000100002674 Contents`closure #2 in  [inlined] generic specialization <Swift.Int, Swift.Bool> of Swift.Dictionary.count.getter : Swift.Int at Contents.swift:0 [opt]
   41   }
   42  
   43   // concurrent access
-> 44   Task {
   45     while true { blackHole(state.dict.count) }
   46   }
   47  
Note: this address is compiler-generated code in function generic specialization <Swift.Int, Swift.Bool> of Swift.Dictionary.count.getter : Swift.Int that has no source code associated with it.
Target 0: (Contents) stopped.
warning: Contents was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x8000000000000010)
  * frame #0: 0x0000000100002674 Contents`closure #2 in  [inlined] generic specialization <Swift.Int, Swift.Bool> of Swift.Dictionary.count.getter : Swift.Int at Contents.swift:0 [opt]
    frame #1: 0x0000000100002674 Contents`closure #2 in  at Contents.swift:45:37 [opt]
    frame #2: 0x0000000100003948 Contents`(1) await resume partial function for partial apply forwarder for closure #1 () async -> () in Contents
    frame #3: 0x0000000100003138 Contents`specialized thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A) at <compiler-generated>:0 [opt]
    frame #4: 0x0000000100003948 Contents`(1) await resume partial function for partial apply forwarder for closure #1 () async -> () in Contents

Expected behavior

The code fails to compile with a concurrency-related diagnostic.

Environment

swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0

Additional information

Removing the await trampoline { … } call correctly prevents compilation:

DataRace.playground/Contents.swift:30:11: error: sending 'self' risks causing data races
28 | 
29 |   func continuouslyModify(isolation: isolated (any Actor)? = #isolation) async {
30 |     await spawnModifyingTasks()
   |           |- error: sending 'self' risks causing data races
   |           `- note: sending 'isolation'-isolated 'self' to nonisolated instance method 'spawnModifyingTasks()' risks causing data races between nonisolated and 'isolation'-isolated uses
31 |   }
32 | }

However, even without the trampoline function, you can still induce a runtime crash by forwarding the actor isolation:

// […]

final class State {
  var dict = [Int: Bool]()

  func spawnModifyingTasks(isolation: isolated (any Actor)?) async {
    let absurd = 0...1_000_000
    await withTaskGroup(of: Bool.self) { group in
      for _ in absurd {
        group.addTask { .random() }
      }

      for await bool in group {
        dict[Int.random(in: absurd)] = bool
      }
    }
  }

  func continuouslyModify(isolation: isolated (any Actor)? = #isolation) async {
    await spawnModifyingTasks(isolation: isolation)
  }
}

// […]

This resultant binary compiled from this code also segfaults.

The trampoline function isn't as contrived as it seems because in the real code where the crash was occurring, trampoline is actually a helper function that provides latency measurements by trivially wrapping async closures.

This issue may very well be a duplicate of #76003 or #76016, but it might help with triage.

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