From b96ee526c1fd24c21c8b115c18096d59272712a9 Mon Sep 17 00:00:00 2001 From: Jamil Date: Tue, 5 Aug 2025 16:08:56 -0400 Subject: [PATCH] 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 --- .../FirezoneNetworkExtension/Adapter.swift | 4 +- .../BindResolvers.swift | 62 +++++++++++-------- website/src/components/Changelog/Apple.tsx | 9 ++- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/swift/apple/FirezoneNetworkExtension/Adapter.swift b/swift/apple/FirezoneNetworkExtension/Adapter.swift index b15237a46..da05c35a4 100644 --- a/swift/apple/FirezoneNetworkExtension/Adapter.swift +++ b/swift/apple/FirezoneNetworkExtension/Adapter.swift @@ -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() diff --git a/swift/apple/FirezoneNetworkExtension/BindResolvers.swift b/swift/apple/FirezoneNetworkExtension/BindResolvers.swift index 3867e1b03..3470e3223 100644 --- a/swift/apple/FirezoneNetworkExtension/BindResolvers.swift +++ b/swift/apple/FirezoneNetworkExtension/BindResolvers.swift @@ -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.. 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.. 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) } } diff --git a/website/src/components/Changelog/Apple.tsx b/website/src/components/Changelog/Apple.tsx index 9b85d6f17..a60d1da62 100644 --- a/website/src/components/Changelog/Apple.tsx +++ b/website/src/components/Changelog/Apple.tsx @@ -24,7 +24,14 @@ export default function Apple() { return ( {/* 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. */} - + + + 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. + + Fixes an issue on iOS where the tunnel may never fully come up after