Race condition - Swift

Race condition - Swift

Need

Prevention of race conditions in the system

Context

  • Usage of Swift for iOS and macOS app development
  • Usage of Foundation for building responsive and accessible web interfaces

Description

Non compliant code

        import Foundation

class SharedResource {
    var data: String = ""
}

class WorkerThread: Thread {
    let sharedResource: SharedResource

    init(sharedResource: SharedResource) {
        self.sharedResource = sharedResource
    }

    override func main() {
        if !sharedResource.data.isEmpty {
            print("Data: \\(sharedResource.data)")
        } else {
            sharedResource.data = "New Data"
        }
    }
}

let sharedResource = SharedResource()
let thread1 = WorkerThread(sharedResource: sharedResource)
let thread2 = WorkerThread(sharedResource: sharedResource)

thread1.start()
thread2.start()
        
        

In the above code, we have a SharedResource class that contains a data property. We have a WorkerThread class that inherits from Thread. Each WorkerThread instance shares the same SharedResource instance.

When the main method of WorkerThread is called, it checks if data property of the shared resource is empty. If it's not, it prints the data, otherwise, it sets the data to "New Data".

The vulnerability lies in the fact that there is no mechanism in place to ensure that the operations (checking if data is empty and setting data to "New Data") on the shared resource are atomic. This can lead to a race condition.

For example, consider the scenario where thread1 checks if data is empty and finds it to be true. Before thread1 can set data to "New Data", thread2 may also check if data is empty and find it to be true. As a result, both threads may end up setting data to "New Data", which could lead to unexpected behavior.

Steps

  • Identify the critical section of code where the shared resource is accessed.
  • Implement a synchronization mechanism to ensure that only one thread can access the shared resource at a time.
  • Use locks, semaphores, or other concurrency control mechanisms to protect the critical section.
  • Ensure that the synchronization mechanism is properly implemented and covers all relevant sections of code.
  • Test the application thoroughly to verify that the race condition vulnerability has been resolved.

Compliant code

        import Foundation

class SharedResource {
    private var internalData: String = ""
    private let accessQueue = DispatchQueue(label: "com.example.SharedResource")

    var data: String {
        get {
            return accessQueue.sync { internalData }
        }
        set {
            accessQueue.sync { internalData = newValue }
        }
    }
}

class WorkerThread: Thread {
    let sharedResource: SharedResource

    init(sharedResource: SharedResource) {
        self.sharedResource = sharedResource
    }

    override func main() {
        if !sharedResource.data.isEmpty {
            print("Data: \\(sharedResource.data)")
        } else {
            sharedResource.data = "New Data"
        }
    }
}

let sharedResource = SharedResource()
let thread1 = WorkerThread(sharedResource: sharedResource)
let thread2 = WorkerThread(sharedResource: sharedResource)

thread1.start()
thread2.start()
        
        

The original code had a race condition vulnerability where two threads could access and modify the shared resource at the same time. This could lead to unexpected behavior if the threads do not execute in the expected order.

The fixed code uses a DispatchQueue to synchronize access to the shared resource. This ensures that only one thread can access the shared resource at a time, eliminating the race condition. The DispatchQueue is a first-in, first-out queue to which work can be submitted. Work submitted to dispatch queues are executed on a pool of threads fully managed by the system. No guarantee is made as to the thread on which a task executes.

In the SharedResource class, a private DispatchQueue accessQueue is created. The data property is modified to use this queue when getting or setting the internalData value. This ensures that these operations are always performed in the order they were received, and only one at a time.

In the WorkerThread class, the main method is unchanged. It will either print the current data if it exists, or set new data if it does not. However, because of the changes to the SharedResource class, these operations are now thread-safe.

Finally, two WorkerThread instances are created and started. These threads will now safely share the SharedResource instance without risk of a race condition.

References