skip to Main Content

I am trying to understand what’t wrong in my approach of access to array. In my app I got several not reproducible crashes with EXC_BAD_ACCESS KERN_INVALID_ADDRESS (some not 0 address). I suggested that it was related to read/write access to array. To investigate I wrote code a bit similar to the app code for playground:

import Foundation
import CoreBluetooth

let requiredServices: [CBUUID] = [
    CBUUID(string: "180A"),
    CBUUID(string: "280A"),
    CBUUID(string: "380A"),
    CBUUID(string: "480A"),
    CBUUID(string: "580A"),
    CBUUID(string: "680A"),
    CBUUID(string: "780A"),
    CBUUID(string: "880A"),
]

// Array to investigate
var availableServices = [CBUUID]()

let queues = [
    DispatchQueue(label: "test0"),
    DispatchQueue(label: "test1"),
    DispatchQueue(label: "test2"),
    DispatchQueue(label: "test3"),
    DispatchQueue(label: "test4"),
    DispatchQueue(label: "test5"),
    DispatchQueue(label: "test6"),
    DispatchQueue(label: "test7"),
    DispatchQueue(label: "test8"),
    DispatchQueue(label: "test9"),
    DispatchQueue(label: "test10")
]

func mainTest() {
    
    print(">>>> Main test")
    
    
    for i in 0...100 {
        for j in 1...10 {
            queues[0].async {
                for _ in 1...100 {
                    // Write operation
                    doAppendAndClear()
                }
            }
            queues[j].async {
                for k in 1...100 {
                    // Read operation
                    tryToRead()
                    if i == 100, j == 10, k == 100 {
                        print(">>>> The end")
                    }
                }
                
            }
        }
    }
}

func doAppendAndClear() {
    availableServices.append(CBUUID(string: "180A"))
    availableServices.append(CBUUID(string: "280A"))
    
    availableServices = []
    
    availableServices.append(CBUUID(string: "380A"))
    availableServices.append(CBUUID(string: "480A"))
    
    availableServices = []
    
    availableServices.append(CBUUID(string: "580A"))
    availableServices.append(CBUUID(string: "680A"))
    
    availableServices = []
    
    availableServices.append(CBUUID(string: "780A"))
    availableServices.append(CBUUID(string: "880A"))
    
    availableServices = []
}

func tryToRead() {
    let services = availableServices
    var available = true
    requiredServices.forEach({ available = available && services.contains($0)})
}

mainTest()

I got this warninig in playground and the same warning when I pasted the test in app code:

Object 0x600000e631e0 of class _ContiguousArrayStorage deallocated with non-zero retain count 2. This object’s deinit, or something called from it, may have created a strong reference to self which outlived deinit, resulting in a dangling reference

Besides that I was getting crashes in the app when I pasted that tested code there. It happened in tryToRead() method, in the last line of it:

thread #17, queue = 'test5', stop reason = EXC_BAD_ACCESS (code=1, address=0x97b30d800)
    frame #0: 0x000000018c3f2244 libobjc.A.dylib`objc_retain_x8 + 16
    frame #1: 0x0000000193172210 libswiftCore.dylib`swift::metadataimpl::ValueWitnesses<swift::metadataimpl::ObjCRetainableBox>::initializeWithCopy(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*) + 28
    frame #2: 0x0000000192ddf844 libswiftCore.dylib`Swift.Array.subscript.read : (Swift.Int) -> τ_0_0 + 232
    frame #3: 0x0000000192ddf728 libswiftCore.dylib`protocol witness for Swift.Collection.subscript.read : (τ_0_0.Index) -> τ_0_0.Element in conformance Swift.Array<τ_0_0> : Swift.Collection in Swift + 68
    frame #4: 0x0000000192e3fad0 libswiftCore.dylib`protocol witness for Swift.IteratorProtocol.next() -> Swift.Optional<τ_0_0.Element> in conformance Swift.IndexingIterator<τ_0_0> : Swift.IteratorProtocol in Swift + 676
    frame #5: 0x0000000192f88164 libswiftCore.dylib`Swift.Sequence.contains(where: (τ_0_0.Element) throws -> Swift.Bool) throws -> Swift.Bool + 664
    frame #6: 0x0000000192e767ec libswiftCore.dylib`Swift.Sequence< where τ_0_0.Element: Swift.Equatable>.contains(τ_0_0.Element) -> Swift.Bool + 156
  * frame #7: 0x0000000104d2a3d4 MyApp`closure #1 in ContainerVC.tryToRead($0=0x0000000303fde400, available=true, services=2 values) at ContainerVC.swift:202:58
    frame #8: 0x0000000104d310d4 MyApp`partial apply for closure #1 in ContainerVC.tryToRead() at <compiler-generated>:0
    frame #9: 0x0000000192ea2e70 libswiftCore.dylib`Swift.Sequence.forEach((τ_0_0.Element) throws -> ()) throws -> () + 756
    frame #10: 0x0000000104d2a2dc MyApp`ContainerVC.tryToRead(self=0x00000001420c7800) at ContainerVC.swift:202:26
    frame #11: 0x0000000104d29c2c MyApp`closure #2 in ContainerVC.mainTest(self=0x00000001420c7800, i=0, j=5) at ContainerVC.swift:166:30

Original crash in firebase has different top lines in stack:

Crashed: com.apple.main-thread
0  libswiftCore.dylib             0x2f7178 _swift_release_dealloc + 16
1  libswiftCore.dylib             0x2f8060 bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 184

So, the question is why that dangling pointes appeared? Also maybe you know the best practices to avoid that when perform read/write to array?

2

Answers


  1. Chosen as BEST ANSWER

    In short, the issue arises when one thread is trying to copy data from an array

    let services = availableServices
    

    the operation might stop in between, because it's not atomic. Then another thread can change availableServices :

    availableServices = []
    

    Then when the first thread will continue it will get corrupted data.

    To avoid this we should implement access to the array in a thread-safe way.


  2. This stack trace, the “dangling pointer” reference, etc., is a red herring. This code is simply not thread-safe. If you do this within Xcode, that offers additional diagnostic tools, notably the Thread Sanitizer (aka TSAN) . If you use TSAN, it will report “Swift access race”, i.e., the thread-safety violation:

    ==================
    WARNING: ThreadSanitizer: Swift access race (pid=51572)
      Modifying access of Swift variable at 0x000110104370 by thread T3:
        #0 …
    
      Previous read of size 8 at 0x000110104370 by thread T4:
        #0 …
    
      Location is heap block of size 144 at 0x0001101042f0 allocated by main thread:
        #0 …
    
      Thread T3 (tid=26151799, running) is a GCD worker thread
    
      Thread T4 (tid=26151800, running) is a GCD worker thread
    
    SUMMARY: ThreadSanitizer: Swift access race (/…/MyApp:arm64+0x100003314) in MyApp.ViewController.availableServices.modify : Swift.Array<__C.CBUUID>+0x84
    ==================
    

    You must synchronize your access to the array in order to ensure thread safety. All interaction must be synchronized, using a mechanism, such as an actor, a lock, or a serial dispatch queue, amongst others. You cannot have any access (whether reads or writes) while a write is underway.

    For information on how we do this with actors, see WWDC 2022 video, Eliminate data races using Swift Concurrency


    At the risk of going into the weeds, let me address a comment you made:

    I thought that copy of the array should save from concurrency issues especially for reading.

    The problem is that you are not dealing with a copy of array. Consider this simplified (!) example:

    private let synchronizationQueue = DispatchQueue(label: "synchronizationQueue")
    private let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)    // a simpler alternative to the array of dispatch queues
    
    var array: [Int] = []
    
    func append(_ value: Int) {
        synchronizationQueue.async {
            self.array.append(value)
            print("appended", value)
        }
    }
    
    #warning("This `read` method is not thread-safe")
    
    func read() {
        concurrentQueue.async {
            print("original value:", array)
            Thread.sleep(forTimeInterval: 1) // we would never do this in production code; just doing this so you can see that we're dealing with the original array
            print("final value:", array)
        }
    }
    
    func mainTest() {
        append(1)
        append(2)
        append(3)
    
        // start `read` in 0.1 seconds; it will take 1 second to complete
    
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.read()
        }
    
        // in the meantime, perform a write; timed such that it
        // will happen while the above `read` is still underway
    
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
            self.append(42)
        }
    }
    

    If you do this within Xcode, it will warn you that it is not array that is captured, but rather self!

    enter image description here

    This is the hint regarding the thread-safety issues (shown in Xcode, but not necessarily playgrounds or the like). To resolve that warning, we can add the explicit self references:

    func read() {
        concurrentQueue.async {
            print("original value:", self.array)
            Thread.sleep(forTimeInterval: 1) // we would never do this in production code; just doing this so you can see that we're dealing with the original array
            print("final value:", self.array)
        }
    }
    

    Fine, that eliminates the warnings, but this results in the following behavior, where you can see that array changed on another thread (!):

    original value: [1, 2, 3]
    appended 42
    final value: [1, 2, 3, 42]    // whoa, the `array` in `read` changed!!!
    

    And if we turn on TSAN, we will receive an error, informing us of the thread-safety violation:

    enter image description here

    To fix this, we must change the read operation to explicitly fetch the value and then use that copy of the array in read operation. E.g., you could use a capture list to make a copy of the array:

    func read() {
        synchronizationQueue.async {
            self.concurrentQueue.async { [array = self.array] in
                print("original value:", array)
                Thread.sleep(forTimeInterval: 1) // we would never do this in production code; just doing this so you can see that we're no longer dealing with the original array
                print("final value:", array)
            }
        }
    }
    

    Or you can manually make your own copy:

    func read() {
        let copiedArray = synchronizationQueue.sync { array }
    
        concurrentQueue.async {
            print("original value:", copiedArray)
            Thread.sleep(forTimeInterval: 1) // we would never do this in production code; just doing this so you can see that we're no longer dealing with the original array
            print("final value:", copiedArray)
        }
    }
    

    In both of these examples, we explicitly use the synchronizationQueue to get a copy of the array. Either way, you will now see the copy of the array will not change while the original array mutated on the other thread. And there will no longer be a warning from TSAN.

    original value: [1, 2, 3]
    appended 42
    final value: [1, 2, 3]        // hurray; even though the original array mutated, the read operation was successfully using its own copy
    

    I confess that this example is a little contrived, but I wanted to illustrate that you do not automatically get a copy of the array and we have to do so manually. There are lots of other ways to make copies of the array, but hopefully this illustrates the underlying problem.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search