fix(apple/ios): don't require hard link to __res_9_state (#10143)

In Xcode 16, how the compiler determines the size of C structs changed.

In Xcode 15 and below, when the compiler saw `__res_9_state()`, it
thought, "This is a C struct. I know its size and layout from the
system's header files. I will generate code to allocate that much memory
and zero it out." This was a type-based operation; it only needed the
"blueprint" for the struct.

In Xcode 16 and later, the compiler sees `__res_9_state()` and thinks,
"This is a C struct. To initialize it, I need to link to the actual
symbol named `___res_9_state` inside the libresolv library." This became
a symbol-based operation, creating a direct dependency that didn't exist
before.

To fix this, we initialize a raw pointer with a manual type
specification to the zeroed-out struct, which reverts to the prior
behavior.

Has been tested on iPhone 12, iOS 17.

Fixes #10108
This commit is contained in:
Jamil
2025-08-05 16:08:56 -04:00
committed by GitHub
parent 15d281d91c
commit b96ee526c1
3 changed files with 47 additions and 28 deletions

View File

@@ -495,7 +495,7 @@ extension Adapter: CallbackHandlerDelegate {
// the system's resolver and we can grab the system resolvers directly.
// If we try to continue below without valid tunnel addresses assigned
// to the interface, we'll crash.
return BindResolvers().getservers().map(BindResolvers.getnameinfo)
return BindResolvers.getServers()
}
var resolvers: [String] = []
@@ -511,7 +511,7 @@ extension Adapter: CallbackHandlerDelegate {
guard let networkSettings = self.networkSettings else { return }
// Only now can we get the system resolvers
resolvers = BindResolvers().getservers().map(BindResolvers.getnameinfo)
resolvers = BindResolvers.getServers()
// Restore connlib's DNS resolvers
networkSettings.clearDummyMatchDomain()

View File

@@ -1,38 +1,51 @@
//
// Resolv.swift
// Firezone
// BindResolvers.swift
// (c) 2024 Firezone, Inc.
// LICENSE: Apache-2.0
//
// Created by Jamil Bou Kheir on 12/22/23.
//
// Reads system resolvers from libresolv, similar to reading /etc/resolv.conf but this also works on iOS
public class BindResolvers {
var state = __res_9_state()
import FirezoneKit
public init() {
res_9_ninit(state)
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) }
}
deinit {
res_9_ndestroy(state)
}
public final func getservers() -> [res_9_sockaddr_union] {
let maxServers = 10
var servers = [res_9_sockaddr_union](repeating: res_9_sockaddr_union(), count: maxServers)
let found = Int(res_9_getservers(state, &servers, Int32(maxServers)))
// filter is to remove the erroneous empty entry when there's no real servers
return Array(servers[0..<found]).filter { $0.sin.sin_len > 0 }
}
}
extension BindResolvers {
public static func getnameinfo(_ sock: res_9_sockaddr_union) -> String {
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(
@@ -42,7 +55,6 @@ extension BindResolvers {
NI_NUMERICHOST)
}
}
return String(cString: hostBuffer)
}
}

View File

@@ -24,7 +24,14 @@ export default function Apple() {
return (
<Entries downloadLinks={downloadLinks} title="macOS / iOS">
{/* When you cut a release, remove any solved issues from the "known issues" lists over in `client-apps`. This must not be done when the issue's PR merges. */}
<Unreleased></Unreleased>
<Unreleased>
<ChangeItem pull="10143">
Fixes an issue on iOS 17 and below that caused the tunnel to crash
after signing in. This was due to a change in how newer versions of
Xcode handle linking against referenced libraries. iOS 18 and higher
is unaffected.
</ChangeItem>
</Unreleased>
<Entry version="1.5.6" date={new Date("2025-08-02")}>
<ChangeItem pull="10075">
Fixes an issue on iOS where the tunnel may never fully come up after