Files
firezone/swift/apple/FirezoneNetworkExtension/BindResolvers.swift
Mariusz Klochowicz bf95dc45a3 refactor(apple): Upgrade to Swift 6.2 with concurrency checks (#10682)
This PR upgrades the Swift client from Swift 5 to Swift 6.2, addressing
all
concurrency-related warnings and runtime crashes that come with Swift
6's
strict concurrency checking.

## Swift 6 Concurrency Primer

**`actor`** - A new reference type that provides thread-safe, serialised
access to mutable state. Unlike classes, actors ensure that only one
piece of
code can access their mutable properties at a time. Access to actor
methods/properties requires await and automatically hops to the actor's
isolated executor.

**`@MainActor`** - An attribute that marks code to run on the main
thread.
Essential for UI updates and anything that touches UIKit/AppKit. When a
class/function is marked @MainActor, all its methods and properties
inherit
this isolation.

**`@Sendable`** - A protocol indicating that a type can be safely passed
across concurrency domains (between actors, tasks, etc.). Value types
(structs, enums) with Sendable stored properties are automatically
Sendable.
Reference types (classes) need explicit @unchecked Sendable if they
manage
thread-safety manually.

**`nonisolated`** - Opts out of the containing type's actor isolation.
For
example, a nonisolated method in a @MainActor class can be called from
any
thread without await. Useful for static methods or thread-safe
operations.

**`@concurrent`** - Used on closure parameters in delegate methods.
Indicates
the closure may be called from any thread, preventing the closure from
inheriting the surrounding context's actor isolation. Critical for
callbacks
from system frameworks that call from background threads.

**Data Races** - Swift 6 enforces at compile-time (and optionally at
runtime)
that mutable state cannot be accessed concurrently from multiple
threads. This
eliminates entire classes of bugs that were previously only caught
through
testing or production crashes.

## Swift Language Upgrade

- **Bump Swift 5 → 6.2**: Enabled strict concurrency checking throughout
the
  codebase
- **Enable ExistentialAny (SE-0335)**: Adds compile-time safety by
making
  protocol type erasure explicit (e.g., any Protocol instead of implicit
  Protocol)
- **Runtime safety configuration**: Added environment variables to log
concurrency violations during development instead of crashing, allowing
  gradual migration

## Concurrency Fixes

### Actor Isolation

- **TelemetryState actor** (Telemetry.swift:10): Extracted mutable
telemetry
state into a dedicated actor to eliminate data races from concurrent
access
- **SessionNotification @MainActor isolation**
(SessionNotification.swift:25):
  Properly isolated the class to MainActor since it manages UI-related
  callbacks
- **IPCClient caching** (IPCClient.swift): Fixed actor re-entrance
issues and
resource hash-based optimisation by caching the client instance in Store

### Thread-Safe Callbacks

- **WebAuthSession @concurrent delegate** (WebAuthSession.swift:46): The
  authentication callback is invoked from a background thread by
ASWebAuthenticationSession. Marked the wrapper function as @concurrent
to
  prevent MainActor inference on the completion handler closure, then
  explicitly hopped back to MainActor for the session.start() call. This
  fixes EXC_BAD_INSTRUCTION crashes at _dispatch_assert_queue_fail.
- **SessionNotification @concurrent delegate**
(SessionNotification.swift:131): Similarly marked the notification
delegate
method as @concurrent and used Task { @MainActor in } to safely invoke
the
  MainActor-isolated signInHandler

### Sendable Conformances

- Added Sendable to Resource, Site, Token, Configuration, and other
model
  types that are passed between actors and tasks
- **LogWriter immutability** (Log.swift): Made jsonData immutable to
prevent
  capturing mutable variables in @Sendable closures

### Nonisolated Methods

- **Static notification display** (SessionNotification.swift:73): Marked
showSignedOutNotificationiOS() as nonisolated since it's called from the
  Network Extension (different process) and only uses thread-safe APIs

Fixes #10674
Fixes #10675
2025-11-05 04:24:49 +00:00

69 lines
2.3 KiB
Swift

//
// BindResolvers.swift
// (c) 2024 Firezone, Inc.
// LICENSE: Apache-2.0
//
//
// Reads system resolvers from libresolv, similar to reading /etc/resolv.conf but this also works on iOS
import FirezoneKit
enum BindResolvers {
static func getServers() -> [String] {
// 1. Manually allocate memory for one __res_9_state struct. On iOS 17 and below, this prevents the linker
// from attempting to link to libresolv9 which prevents an "Symbol not found" error.
// See https://github.com/firezone/firezone/issues/10108
let statePtr = UnsafeMutablePointer<__res_9_state>.allocate(capacity: 1)
statePtr.initialize(to: __res_9_state()) // Zero-initialize the allocated memory.
// 2. Ensure memory is cleaned up.
defer {
res_9_ndestroy(statePtr)
statePtr.deinitialize(count: 1)
statePtr.deallocate()
}
// 3. Initialize the resolver state by passing the pointer directly.
guard res_9_ninit(statePtr) == 0 else {
Log.warning("Failed to initialize resolver state")
// Cleanup will happen via defer.
return []
}
// 4. Get the servers.
var servers = [res_9_sockaddr_union](repeating: res_9_sockaddr_union(), count: 10)
let foundCount = Int(res_9_getservers(statePtr, &servers, Int32(servers.count)))
// 5. Process the results.
let validServers = Array(servers[0..<foundCount]).filter { $0.sin.sin_len > 0 }
return validServers.map { getnameinfo($0) }
}
private static func getnameinfo(_ sock: res_9_sockaddr_union) -> String {
var sockUnion = sock
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let sinlen = socklen_t(sockUnion.sin.sin_len)
_ = withUnsafePointer(to: &sockUnion) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
Darwin.getnameinfo(
$0, sinlen,
&hostBuffer, socklen_t(hostBuffer.count),
nil, 0,
NI_NUMERICHOST)
}
}
// Truncate null termination and decode as UTF-8
// Convert CChar (Int8) to UInt8 for String(decoding:)
if let nullIndex = hostBuffer.firstIndex(of: 0) {
let bytes = hostBuffer[..<nullIndex].map { UInt8(bitPattern: $0) }
return String(decoding: bytes, as: UTF8.self)
} else {
let bytes = hostBuffer.map { UInt8(bitPattern: $0) }
return String(decoding: bytes, as: UTF8.self)
}
}
}