Shared Mutable State
ID |
swift.shared_mutable_state |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Synchronization |
Language |
Swift |
Tags |
ASVS50:v15.4.1, CWE:362, CWE:567, NIST.SP.800-53, PCI-DSS:6.5.6 |
Description
Detects shared mutable state — global variables, static properties, and the properties of the one instance a singleton holds — that is written outside initialization code. A server such as Vapor runs its route handlers concurrently on its event loops, so this state is read and written by many requests at the same time, creating potential race conditions.
A singleton is a class with a static let shared = Registry(), or a global constant holding an instance of a class the file declares.
When the state is assigned a value read from a function or closure parameter, concurrent requests hand one caller’s data to another, and that finding is raised to high. When it is a counter or a collection written concurrently, updates are lost and the collection can be corrupted.
One issue is reported per piece of state, at its worst write.
Rationale
A global array a handler appends to:
var recentSearches: [String] = []
func remember(_ req: Request) {
recentSearches.append(req.url.string) // every concurrent request writes the same array
}
A static counter:
enum HitCounter {
static var hits = 0
}
app.get("hit") { req -> String in
HitCounter.hits += 1 // read-modify-write is not atomic
return "ok"
}
A singleton written per request:
final class SessionCache {
static let shared = SessionCache()
var sessions: [String: String] = [:]
func put(_ id: String, user: String) {
sessions[id] = user // one instance for the whole process
}
}
Remediation
-
Eliminate Shared State: keep request data in the request (
req.storage), in arguments and return values, or in a service built per request. -
Use an Actor: an actor isolates its state, and its callers reach it one at a time:
actor SessionStore { var sessions: [String: String] = [:] func put(_ id: String, user: String) { sessions[id] = user } } -
Use Thread-Safe Types or a Lock:
ManagedAtomicfor counters,NIOLockedValueBoxorMutexfor values, or a lock held around every access (lock.withLock { … }).
Not Reported
-
The state of an actor, and state isolated to a global actor (
@MainActor) or local to a task (@TaskLocal). -
A thread-safe holder (
ManagedAtomic,NIOLockedValueBox,Mutex,OSAllocatedUnfairLock). -
A write inside a closure a lock or a serial queue runs (
lock.withLock { … },queue.sync { … }), or after the function takes a lock (lock.lock()). -
A lazy initialization,
if x == nil { x = … }or afterif let v = x { return v }, whose value is not a parameter. -
A write in an initializer, a deinitializer, or a function that boots the application (
configure(_:)). -
Test code, and client code: a file importing SwiftUI, UIKit or AppKit serves one user.