EXC_BAD_ACCESS when using `Task.detached` on a class

I'm having hard time understanding where multiple crashes are coming from this simple code.

typealias RepositoryData = Set<RepositoryItem> //RepositoryItem is a Sendable, struct
final class Repository {
  private var data: RepositoryData
  
  func save() {
      let task = Task.detached { [weak self] in
         try await Task.sleep(seconds: 1)
         self?.data.save()
      }
  }
}

private extension Data {
  func save() {
    do {
      let eventJSONData = try JSONEncoder().encode(self)
      UserDefaults.standard.set(eventJSONData, forKey: "data")
    } catch {}
  }
}

I want to save data to UserDefaults after 1sec delay. Meanwhile, save Task is sleeping, there are some operations that can happen on the data itself (some added, some removed).
I cannot reproduce the issues, but in crash reports from production code I'm getting multiple different errors:

  • Attempted to dereference garbage pointer
  • countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance 0x8000000000000000
  • NSInvalidArgumentException countByEnumeratingWithState:objects:count
    coming from this line: let eventJSONData = try JSONEncoder().encode(self)

According to what I found JSONEncoder is Thread Safe and I cannot figure out why save() method is called properly, but JSONEncoder is crashing when accessing save() - it's not the save() method itself as all logs from this method are correctly visible. So data exists when save() is called, but then when Encoder comes in action it somehow gets deallocated.
What am I missing here?

Assuming this is accurate, would withExtendedLifetime fix this?

withExtendedLifetime(self) { data in
  do {
    let eventJSONData = try JSONEncoder().encode(data)
    UserDefaults.standard.set(eventJSONData, forKey: "data")
  } catch {}
}

It shouldn't be necessary of course, this is weird, but if your analysis is correct that should fix it.

Could the issue be the mutability of the data property itself? It seems like there is some potential for racing on the data access. Have you tried running with the thread sanitizer enabled?

Yes, this is a thread-safety bug. Making Repository an actor would resolve it.