From 4b62d73d47b71f3113b91ab228dad7cfc3a317d0 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 29 Apr 2026 15:00:32 -0700 Subject: [PATCH 01/12] Migrate Apple reachability from SCNetworkReachability to NWPathMonitor Replace the deprecated SCNetworkReachability APIs with NWPathMonitor for modern Apple deployment targets (iOS 12+, macOS 10.14+). The legacy SCNetworkReachability path is retained behind a compile-time check for older targets. Changes: - NetworkInformationImpl.mm: refactor to use NWPathMonitor as the primary reachability mechanism, with SCNetworkReachability as fallback for older deployment targets only - ODWReachability.h/m: add NWPathMonitor-based implementation gated on availability, keeping SCNetworkReachability for backward compatibility - Remove dead private header imports from tests This eliminates the -Wdeprecated-declarations build failures on Xcode 26.4+ without needing pragma suppressions. Fixes #1425 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/pal/posix/NetworkInformationImpl.mm | 220 ++++++++++-------- tests/unittests/obj-c/ODWReachabilityTests.mm | 2 - third_party/Reachability/ODWReachability.h | 12 + third_party/Reachability/ODWReachability.m | 57 ++++- 4 files changed, 188 insertions(+), 103 deletions(-) diff --git a/lib/pal/posix/NetworkInformationImpl.mm b/lib/pal/posix/NetworkInformationImpl.mm index 24c7839eb..943c22667 100644 --- a/lib/pal/posix/NetworkInformationImpl.mm +++ b/lib/pal/posix/NetworkInformationImpl.mm @@ -20,7 +20,7 @@ class NetworkInformation : public NetworkInformationImpl, public std::enable_shared_from_this { - public: + public: /// /// /// @@ -59,21 +59,27 @@ virtual NetworkCost GetNetworkCost() /// /// Setup initial network information and start net monitor if requested. /// This cannot be put in constructor because we need to use shared_from_this. - /// - void SetupNetDetect(); + /// + void SetupNetDetect(); - private: - void UpdateType(NetworkType type) noexcept; - void UpdateCost(NetworkCost cost) noexcept; - std::string m_network_provider {}; + private: + void SetupModernNetDetect() API_AVAILABLE(macos(10.14), ios(12.0)); +#if ODW_LEGACY_REACHABILITY_REQUIRED + void SetupLegacyNetDetect(); +#endif + void UpdateType(NetworkType type) noexcept; + void UpdateCost(NetworkCost cost) noexcept; + std::string m_network_provider {}; - // iOS 12 and newer - nw_path_monitor_t m_monitor = nil; + // iOS 12+ / macOS 10.14+ + nw_path_monitor_t m_monitor = nil; - // iOS 11 and older - ODWReachability* m_reach = nil; - id m_notificationId = nil; - }; +#if ODW_LEGACY_REACHABILITY_REQUIRED + // Older Apple deployment targets still need the legacy fallback. + ODWReachability* m_reach = nil; + id m_notificationId = nil; +#endif + }; NetworkInformation::NetworkInformation(IRuntimeConfig& configuration) : NetworkInformationImpl(configuration) @@ -84,6 +90,7 @@ virtual NetworkCost GetNetworkCost() NetworkInformation::~NetworkInformation() noexcept { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { if (m_isNetDetectEnabled) @@ -99,108 +106,130 @@ virtual NetworkCost GetNetworkCost() [m_reach stopNotifier]; } } +#else + if (m_isNetDetectEnabled) + { + nw_path_monitor_cancel(m_monitor); + } +#endif } - void NetworkInformation::SetupNetDetect() + void NetworkInformation::SetupModernNetDetect() { - if (@available(macOS 10.14, iOS 12.0, *)) + auto weak_this = std::weak_ptr(shared_from_this()); + + m_monitor = nw_path_monitor_create(); + nw_path_monitor_set_queue(m_monitor, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); + nw_path_monitor_set_update_handler(m_monitor, ^(nw_path_t path) { - auto weak_this = std::weak_ptr(shared_from_this()); + auto strong_this = weak_this.lock(); + if (!strong_this) + { + return; + } - m_monitor = nw_path_monitor_create(); - nw_path_monitor_set_queue(m_monitor, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); - nw_path_monitor_set_update_handler(m_monitor, ^(nw_path_t path) + NetworkType type = NetworkType_Unknown; + NetworkCost cost = NetworkCost_Unknown; + nw_path_status_t status = nw_path_get_status(path); + bool connected = status == nw_path_status_satisfied || status == nw_path_status_satisfiable; + if (connected) { - auto strong_this = weak_this.lock(); - if (!strong_this) + if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) { - return; + type = NetworkType_Wifi; } - - NetworkType type = NetworkType_Unknown; - NetworkCost cost = NetworkCost_Unknown; - nw_path_status_t status = nw_path_get_status(path); - bool connected = status == nw_path_status_satisfied || status == nw_path_status_satisfiable; - if (connected) + else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) { - if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) - { - type = NetworkType_Wifi; - } - else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) - { - type = NetworkType_WWAN; - } - else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) - { - type = NetworkType_Wired; - } - cost = nw_path_is_expensive(path) ? NetworkCost_Metered : NetworkCost_Unmetered; - if (@available(macOS 10.15, iOS 13.0, *)) + type = NetworkType_WWAN; + } + else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) + { + type = NetworkType_Wired; + } + cost = nw_path_is_expensive(path) ? NetworkCost_Metered : NetworkCost_Unmetered; + if (@available(macOS 10.15, iOS 13.0, *)) + { + if (nw_path_is_constrained(path)) { - if (nw_path_is_constrained(path)) - { - cost = NetworkCost_Roaming; - } + cost = NetworkCost_Roaming; } } - strong_this->UpdateType(type); - strong_this->UpdateCost(cost); - }); - nw_path_monitor_start(m_monitor); - - // nw_path_monitor_start will invoke the callback for once. So if - // we don't want to listen for changes, we can just start the - // monitor and stop it right away. - if (!m_isNetDetectEnabled) - { - nw_path_monitor_cancel(m_monitor); } - } - else + strong_this->UpdateType(type); + strong_this->UpdateCost(cost); + }); + nw_path_monitor_start(m_monitor); + + // nw_path_monitor_start will invoke the callback for once. So if + // we don't want to listen for changes, we can just start the + // monitor and stop it right away. + if (!m_isNetDetectEnabled) { - auto weak_this = std::weak_ptr(shared_from_this()); + nw_path_monitor_cancel(m_monitor); + } + } - m_reach = [ODWReachability reachabilityForInternetConnection]; - void (^block)(NSNotification*) = ^(NSNotification*) - { - auto strong_this = weak_this.lock(); - if (!strong_this) - { - return; - } +#if ODW_LEGACY_REACHABILITY_REQUIRED + void NetworkInformation::SetupLegacyNetDetect() + { + auto weak_this = std::weak_ptr(shared_from_this()); - // NetworkCost information is not available until iOS 12. - // Just make the best guess here. - switch (m_reach.currentReachabilityStatus) - { - case NotReachable: - strong_this->UpdateType(NetworkType_Unknown); - strong_this->UpdateCost(NetworkCost_Unknown); - break; - case ReachableViaWiFi: - strong_this->UpdateType(NetworkType_Wifi); - strong_this->UpdateCost(NetworkCost_Unmetered); - break; - case ReachableViaWWAN: - strong_this->UpdateType(NetworkType_WWAN); - strong_this->UpdateCost(NetworkCost_Metered); - break; - } - }; - block(nil); // Update the initial status. + m_reach = [ODWReachability reachabilityForInternetConnection]; + void (^block)(NSNotification*) = ^(NSNotification*) + { + auto strong_this = weak_this.lock(); + if (!strong_this) + { + return; + } - if (m_isNetDetectEnabled) + // NetworkCost information is not available until iOS 12. + // Just make the best guess here. + switch (m_reach.currentReachabilityStatus) { - m_notificationId = - [[NSNotificationCenter defaultCenter] - addObserverForName: kNetworkReachabilityChangedNotification - object: nil - queue: nil - usingBlock: block]; - [m_reach startNotifier]; + case NotReachable: + strong_this->UpdateType(NetworkType_Unknown); + strong_this->UpdateCost(NetworkCost_Unknown); + break; + case ReachableViaWiFi: + strong_this->UpdateType(NetworkType_Wifi); + strong_this->UpdateCost(NetworkCost_Unmetered); + break; + case ReachableViaWWAN: + strong_this->UpdateType(NetworkType_WWAN); + strong_this->UpdateCost(NetworkCost_Metered); + break; } + }; + block(nil); // Update the initial status. + + if (m_isNetDetectEnabled) + { + m_notificationId = + [[NSNotificationCenter defaultCenter] + addObserverForName: kNetworkReachabilityChangedNotification + object: nil + queue: nil + usingBlock: block]; + [m_reach startNotifier]; + } + } +#endif + + void NetworkInformation::SetupNetDetect() + { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + SetupModernNetDetect(); + } + else + { + SetupLegacyNetDetect(); } +#else + SetupModernNetDetect(); +#endif } void NetworkInformation::UpdateType(NetworkType type) noexcept @@ -229,4 +258,3 @@ virtual NetworkCost GetNetworkCost() } } PAL_NS_END - diff --git a/tests/unittests/obj-c/ODWReachabilityTests.mm b/tests/unittests/obj-c/ODWReachabilityTests.mm index cc8a7c24e..d231bbec8 100644 --- a/tests/unittests/obj-c/ODWReachabilityTests.mm +++ b/tests/unittests/obj-c/ODWReachabilityTests.mm @@ -11,8 +11,6 @@ #import #import "ODWReachability.h" -#import -#import #import @interface ODWReachabilityTests : XCTestCase diff --git a/third_party/Reachability/ODWReachability.h b/third_party/Reachability/ODWReachability.h index b2d218417..57f3702f9 100644 --- a/third_party/Reachability/ODWReachability.h +++ b/third_party/Reachability/ODWReachability.h @@ -25,8 +25,10 @@ POSSIBILITY OF SUCH DAMAGE. */ +#import #import #import +#import /** @@ -40,6 +42,16 @@ extern NSString* const kNetworkReachabilityChangedNotification; +// Older Apple deployment targets still need the legacy SCNetworkReachability +// backend at runtime. Newer targets can compile directly to the modern path. +#if TARGET_OS_IPHONE +#define ODW_LEGACY_REACHABILITY_REQUIRED (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0) +#elif TARGET_OS_OSX +#define ODW_LEGACY_REACHABILITY_REQUIRED (__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_14) +#else +#define ODW_LEGACY_REACHABILITY_REQUIRED 0 +#endif + typedef NS_ENUM(NSInteger, ODWNetworkStatus) { // Apple NetworkStatus Compatible Names. NotReachable = 0, diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index 7947f7df0..dc8deb38f 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -27,8 +27,6 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import "ODWReachability.h" -#import -#import #import @@ -43,6 +41,8 @@ @interface ODWReachability () -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; ++(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:(NSError *)error url:(NSURL *)url; +-(SCNetworkReachabilityFlags)checkNetworkReachability:(BOOL)checkData; @end @@ -65,6 +65,7 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } +#if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { @@ -79,6 +80,7 @@ static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkRea [reachability reachabilityChanged:flags]; } } +#endif @implementation ODWReachability @@ -99,8 +101,10 @@ +(ODWReachability*)reachabilityWithHostName:(NSString*)hostname +(instancetype)reachabilityWithHostname:(NSString*)hostname { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher NSString *formattedHostname = hostname; if (![formattedHostname hasPrefix:@"https://"] && ![formattedHostname hasPrefix:@"http://"]) { @@ -116,6 +120,7 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname }]; [dataTask resume]; return reachabilityInstance; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // Use SCNetworkReachability for macOS 10.14 or lower @@ -131,6 +136,7 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname } return nil; +#endif } +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress @@ -140,8 +146,10 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress return nil; } +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)hostAddress)->sin_addr)]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", addressString]]; @@ -153,6 +161,7 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress }]; [dataTask resume]; return reachabilityInstance; // Return the instance after resuming the data task +#if ODW_LEGACY_REACHABILITY_REQUIRED } // Use SCNetworkReachability for macOS 10.14 or lower @@ -168,6 +177,7 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress } return nil; +#endif } +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:(NSError *)error url:(NSURL *)url @@ -284,8 +294,10 @@ -(void)dealloc -(BOOL)startNotifier { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { @@ -304,6 +316,7 @@ -(BOOL)startNotifier NSLog(@"Failed to create URLSessionDataTask"); return NO; } +#if ODW_LEGACY_REACHABILITY_REQUIRED } // Use SCNetworkReachability for macOS 10.14 or lower @@ -346,14 +359,19 @@ -(BOOL)startNotifier // if we get here we fail at the internet self.reachabilityObject = nil; return NO; +#endif } -(void)stopNotifier { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher, no specific action is needed for URLSession self.reachabilityObject = nil; +#if ODW_LEGACY_REACHABILITY_REQUIRED + return; } // Use SCNetworkReachability for macOS 10.14 or lower @@ -366,6 +384,7 @@ -(void)stopNotifier SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL); #pragma clang diagnostic pop self.reachabilityObject = nil; +#endif } @@ -408,9 +427,12 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags -(BOOL)isReachable { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:true]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -423,11 +445,13 @@ -(BOOL)isReachable #pragma clang diagnostic pop return [self isReachableWithFlags:flags]; +#endif } -(BOOL)isReachableViaWWAN { +#if ODW_LEGACY_REACHABILITY_REQUIRED #if TARGET_OS_IPHONE SCNetworkReachabilityFlags flags = 0; @@ -447,13 +471,19 @@ -(BOOL)isReachableViaWWAN #endif return NO; +#else + return NO; +#endif } -(BOOL)isReachableViaWiFi { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:true]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -479,6 +509,7 @@ -(BOOL)isReachableViaWiFi } return NO; +#endif } @@ -491,9 +522,12 @@ -(BOOL)isConnectionRequired -(BOOL)connectionRequired { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:false]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -508,15 +542,19 @@ -(BOOL)connectionRequired } return NO; +#endif } // Dynamic, on demand connection? -(BOOL)isConnectionOnDemand { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:true]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -532,15 +570,19 @@ -(BOOL)isConnectionOnDemand } return NO; +#endif } // Is user intervention required? -(BOOL)isInterventionRequired { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:false]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -556,6 +598,7 @@ -(BOOL)isInterventionRequired } return NO; +#endif } @@ -579,11 +622,13 @@ -(ODWNetworkStatus)currentReachabilityStatus -(SCNetworkReachabilityFlags)reachabilityFlags { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif __block SCNetworkReachabilityFlags flags = 0; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - + NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error == nil && data != nil) { @@ -591,11 +636,12 @@ -(SCNetworkReachabilityFlags)reachabilityFlags } dispatch_semaphore_signal(semaphore); }]; - + [task resume]; dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); - + return flags; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -610,6 +656,7 @@ -(SCNetworkReachabilityFlags)reachabilityFlags } return 0; +#endif } -(NSString*)currentReachabilityString From 3906d9ef0f1f6fdaff5ce6297b697439bdfdd667 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 29 Apr 2026 15:32:41 -0700 Subject: [PATCH 02/12] Restore ODWReachabilityTests includes to keep #1431 focused PR #1431 should not carry changes in ODWReachabilityTests.mm. Restore the socket header imports so the branch only contains the reachability implementation changes. Files changed: - tests/unittests/obj-c/ODWReachabilityTests.mm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/unittests/obj-c/ODWReachabilityTests.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unittests/obj-c/ODWReachabilityTests.mm b/tests/unittests/obj-c/ODWReachabilityTests.mm index d231bbec8..cc8a7c24e 100644 --- a/tests/unittests/obj-c/ODWReachabilityTests.mm +++ b/tests/unittests/obj-c/ODWReachabilityTests.mm @@ -11,6 +11,8 @@ #import #import "ODWReachability.h" +#import +#import #import @interface ODWReachabilityTests : XCTestCase From 12d89582375126d4382886a5d85b962603c4f991 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 29 Apr 2026 15:34:13 -0700 Subject: [PATCH 03/12] Restore ODWReachability.m header section to match main Keep PR #1431 focused on the reachability implementation changes by reverting the top-of-file/header-area edits in ODWReachability.m. Files changed: - third_party/Reachability/ODWReachability.m Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index dc8deb38f..38faf54d5 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -27,6 +27,8 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import "ODWReachability.h" +#import +#import #import @@ -41,8 +43,6 @@ @interface ODWReachability () -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; -+(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:(NSError *)error url:(NSURL *)url; --(SCNetworkReachabilityFlags)checkNetworkReachability:(BOOL)checkData; @end @@ -65,7 +65,6 @@ -(SCNetworkReachabilityFlags)checkNetworkReachability:(BOOL)checkData; (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } -#if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { @@ -80,7 +79,6 @@ static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkRea [reachability reachabilityChanged:flags]; } } -#endif @implementation ODWReachability From 770250810e3e0067db8340e4814fb2661513136c Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 1 May 2026 19:20:16 -0500 Subject: [PATCH 04/12] Use NWPathMonitor in ODWReachability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 341 +++++++++++++++++---- 1 file changed, 286 insertions(+), 55 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index 38faf54d5..a24c4506e 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -27,22 +27,48 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import "ODWReachability.h" +#import + #import #import #import +#import NSString *const kNetworkReachabilityChangedNotification = @"NetworkReachabilityChangedNotification"; +@class ODWReachability; + +@interface ODWReachabilityMonitorContext : NSObject + +@property (nonatomic, assign) ODWReachability *owner; + +@end + +@implementation ODWReachabilityMonitorContext +@end + @interface ODWReachability () @property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef; @property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue; @property (nonatomic, strong) id reachabilityObject; +@property (nonatomic, strong) nw_path_monitor_t pathMonitor; +@property (nonatomic, strong) ODWReachabilityMonitorContext *pathMonitorContext; +@property (nonatomic, strong) dispatch_semaphore_t initialPathSemaphore; +@property (nonatomic, assign) nw_path_status_t currentPathStatus; +@property (nonatomic, assign) BOOL currentPathUsesWiFi; +@property (nonatomic, assign) BOOL currentPathUsesWWAN; +@property (nonatomic, assign) BOOL hasObservedPath; +@property (nonatomic, assign) BOOL monitorLocalWiFiOnly; -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; +-(BOOL)ensureModernPathMonitor; +-(BOOL)awaitModernPathSnapshot; +-(void)handleModernPathUpdate:(nw_path_t)path; +-(void)notifyModernPathChange; @end @@ -62,9 +88,36 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', - (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; +} + +static BOOL ODWModernPathIsReachable(nw_path_status_t status) +{ + return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; +} + +static BOOL ODWHostResolves(NSString *hostname) +{ + if (hostname == nil || hostname.length == 0) + { + return NO; + } + + struct addrinfo hints = {}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo *result = NULL; + int lookupResult = getaddrinfo(hostname.UTF8String, NULL, &hints, &result); + if (result != NULL) + { + freeaddrinfo(result); + } + + return lookupResult == 0; } +#if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { @@ -79,6 +132,7 @@ static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkRea [reachability reachabilityChanged:flags]; } } +#endif @implementation ODWReachability @@ -103,20 +157,20 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher + // Use Network.framework reachability for macOS 10.14 or higher. NSString *formattedHostname = hostname; if (![formattedHostname hasPrefix:@"https://"] && ![formattedHostname hasPrefix:@"http://"]) { formattedHostname = [NSString stringWithFormat:@"https://%@", hostname]; } NSURL *url = [NSURL URLWithString:formattedHostname]; + if (url == nil || url.host == nil || !ODWHostResolves(url.host)) + { + NSLog(@"Invalid hostname"); + return nil; + } - NSURLSession *session = [NSURLSession sharedSession]; - __block ODWReachability *reachabilityInstance = [[self alloc] init]; + ODWReachability *reachabilityInstance = [[self alloc] init]; reachabilityInstance.url = url; - NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - reachabilityInstance = [self handleReachabilityResponse:response error:error url:reachabilityInstance.url]; - }]; - [dataTask resume]; return reachabilityInstance; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -148,17 +202,19 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher - NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)hostAddress)->sin_addr)]; + // Use Network.framework reachability for macOS 10.14 or higher. + struct sockaddr_in *address = (struct sockaddr_in *)hostAddress; + if (address->sin_addr.s_addr == INADDR_ANY) + { + NSLog(@"Invalid address"); + return nil; + } + + NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(address->sin_addr)]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", addressString]]; - NSURLSession *session = [NSURLSession sharedSession]; - __block ODWReachability *reachabilityInstance = [[self alloc] init]; + ODWReachability *reachabilityInstance = [[self alloc] init]; reachabilityInstance.url = url; - NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - reachabilityInstance = [self handleReachabilityResponse:response error:error url:reachabilityInstance.url]; - }]; - [dataTask resume]; - return reachabilityInstance; // Return the instance after resuming the data task + return reachabilityInstance; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -207,6 +263,13 @@ +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:( +(ODWReachability *)reachabilityForInternetConnection { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + return [[self alloc] init]; + } +#endif + struct sockaddr_in zeroAddress; bzero(&zeroAddress, sizeof(zeroAddress)); zeroAddress.sin_len = sizeof(zeroAddress); @@ -217,6 +280,15 @@ +(ODWReachability *)reachabilityForInternetConnection +(ODWReachability*)reachabilityForLocalWiFi { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + ODWReachability *reachability = [[self alloc] init]; + reachability.monitorLocalWiFiOnly = YES; + return reachability; + } +#endif + struct sockaddr_in localWifiAddress; bzero(&localWifiAddress, sizeof(localWifiAddress)); localWifiAddress.sin_len = sizeof(localWifiAddress); @@ -230,23 +302,139 @@ +(ODWReachability*)reachabilityForLocalWiFi // Initialization methods --(ODWReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +-(instancetype)init { self = [super init]; if (self != nil) { self.reachableOnWWAN = YES; - self.reachabilityRef = ref; + self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); + self.currentPathStatus = nw_path_status_invalid; + } - // We need to create a serial queue. - // We allocate this once for the lifetime of the notifier. + return self; +} - self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); +-(ODWReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +{ + self = [self init]; + if (self != nil) + { + self.reachabilityRef = ref; } return self; } +-(BOOL)ensureModernPathMonitor +{ + if (self.pathMonitor != nil) + { + return YES; + } + + self.hasObservedPath = NO; + self.currentPathStatus = nw_path_status_invalid; + self.currentPathUsesWiFi = NO; + self.currentPathUsesWWAN = NO; + self.initialPathSemaphore = dispatch_semaphore_create(0); + self.pathMonitor = self.monitorLocalWiFiOnly + ? nw_path_monitor_create_with_type(nw_interface_type_wifi) + : nw_path_monitor_create(); + + if (self.pathMonitor == nil) + { + return NO; + } + + ODWReachabilityMonitorContext *context = [[ODWReachabilityMonitorContext alloc] init]; + context.owner = self; + self.pathMonitorContext = context; +#if !__has_feature(objc_arc) + [context release]; +#endif + + nw_path_monitor_set_queue(self.pathMonitor, self.reachabilitySerialQueue); + nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) { + ODWReachability *owner = context.owner; + if (owner == nil) + { + return; + } + + [owner handleModernPathUpdate:path]; + }); + nw_path_monitor_start(self.pathMonitor); + + return YES; +} + +-(BOOL)awaitModernPathSnapshot +{ + if (![self ensureModernPathMonitor]) + { + return NO; + } + + if (self.hasObservedPath) + { + return YES; + } + + if (self.initialPathSemaphore == nil) + { + return NO; + } + + long waitResult = dispatch_semaphore_wait( + self.initialPathSemaphore, + dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); + return waitResult == 0 && self.hasObservedPath; +} + +-(void)handleModernPathUpdate:(nw_path_t)path +{ + self.currentPathStatus = nw_path_get_status(path); + self.currentPathUsesWiFi = nw_path_uses_interface_type(path, nw_interface_type_wifi); +#if TARGET_OS_IPHONE + self.currentPathUsesWWAN = nw_path_uses_interface_type(path, nw_interface_type_cellular); +#else + self.currentPathUsesWWAN = NO; +#endif + + BOOL firstPath = !self.hasObservedPath; + self.hasObservedPath = YES; + if (firstPath && self.initialPathSemaphore != nil) + { + dispatch_semaphore_signal(self.initialPathSemaphore); + } + + if (self.reachabilityObject == self) + { + [self notifyModernPathChange]; + } +} + +-(void)notifyModernPathChange +{ + if (ODWModernPathIsReachable(self.currentPathStatus)) + { + if (self.reachableBlock) + { + self.reachableBlock(self); + } + } + else if (self.unreachableBlock) + { + self.unreachableBlock(self); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkReachabilityChangedNotification + object:self]; + }); +} + +(void)setTimeoutDurationInSeconds:(int)timeoutDuration { if (timeoutDuration >= kTimeoutDurationInSeconds) @@ -296,24 +484,17 @@ -(BOOL)startNotifier if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher - NSURLSession *session = [NSURLSession sharedSession]; - NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error) { - NSLog(@"URLSession failed: %@", error.localizedDescription); - self.reachabilityObject = nil; - } else { - self.reachabilityObject = self; - [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkReachabilityChangedNotification object:self]; + // Use NWPathMonitor for macOS 10.14 or higher. + if ([self ensureModernPathMonitor]) + { + self.reachabilityObject = self; + if ([self awaitModernPathSnapshot]) + { + [self notifyModernPathChange]; } - }]; - if (task) { - [task resume]; return YES; - } else { - NSLog(@"Failed to create URLSessionDataTask"); - return NO; } + return NO; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -366,8 +547,20 @@ -(void)stopNotifier if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher, no specific action is needed for URLSession + // Use NWPathMonitor for macOS 10.14 or higher. self.reachabilityObject = nil; + if (self.pathMonitor != nil) + { + self.pathMonitorContext.owner = nil; + nw_path_monitor_cancel(self.pathMonitor); + self.pathMonitor = nil; + } + self.pathMonitorContext = nil; + self.initialPathSemaphore = nil; + self.hasObservedPath = NO; + self.currentPathStatus = nw_path_status_invalid; + self.currentPathUsesWiFi = NO; + self.currentPathUsesWWAN = NO; #if ODW_LEGACY_REACHABILITY_REQUIRED return; } @@ -429,7 +622,7 @@ -(BOOL)isReachable if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:true]; + return [self awaitModernPathSnapshot] && ODWModernPathIsReachable(self.currentPathStatus); #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -450,6 +643,17 @@ -(BOOL)isReachable -(BOOL)isReachableViaWWAN { #if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { +#if TARGET_OS_IPHONE + return [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; +#else + return NO; +#endif + } + #if TARGET_OS_IPHONE SCNetworkReachabilityFlags flags = 0; @@ -469,9 +673,16 @@ -(BOOL)isReachableViaWWAN #endif return NO; +#endif +#if !ODW_LEGACY_REACHABILITY_REQUIRED +#if TARGET_OS_IPHONE + return [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; #else return NO; #endif +#endif } -(BOOL)isReachableViaWiFi @@ -480,7 +691,20 @@ -(BOOL)isReachableViaWiFi if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:true]; + if (![self awaitModernPathSnapshot] || !ODWModernPathIsReachable(self.currentPathStatus)) + { + return NO; + } +#if TARGET_OS_IPHONE + if (self.monitorLocalWiFiOnly) + { + return self.currentPathUsesWiFi; + } + + return !self.currentPathUsesWWAN; +#else + return self.monitorLocalWiFiOnly ? self.currentPathUsesWiFi : YES; +#endif #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -524,7 +748,8 @@ -(BOOL)connectionRequired if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:false]; + return [self awaitModernPathSnapshot] && + self.currentPathStatus == nw_path_status_satisfiable; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -551,7 +776,7 @@ -(BOOL)isConnectionOnDemand if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:true]; + return NO; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -579,7 +804,7 @@ -(BOOL)isInterventionRequired if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:false]; + return NO; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -624,20 +849,26 @@ -(SCNetworkReachabilityFlags)reachabilityFlags if (@available(macOS 10.14, iOS 12.0, *)) { #endif - __block SCNetworkReachabilityFlags flags = 0; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - - NSURLSession *session = [NSURLSession sharedSession]; - NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error == nil && data != nil) { - flags = kSCNetworkReachabilityFlagsReachable; + if (![self awaitModernPathSnapshot]) + { + return 0; } - dispatch_semaphore_signal(semaphore); - }]; - - [task resume]; - dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); + SCNetworkReachabilityFlags flags = 0; + if (ODWModernPathIsReachable(self.currentPathStatus)) + { + flags |= kSCNetworkReachabilityFlagsReachable; + } + if (self.currentPathStatus == nw_path_status_satisfiable) + { + flags |= kSCNetworkReachabilityFlagsConnectionRequired; + } +#if TARGET_OS_IPHONE + if (self.currentPathUsesWWAN) + { + flags |= kSCNetworkReachabilityFlagsIsWWAN; + } +#endif return flags; #if ODW_LEGACY_REACHABILITY_REQUIRED } From 675cad4e2dcab9803fcc3f2847ff52522a5eeee3 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 2 May 2026 00:38:54 -0500 Subject: [PATCH 05/12] Clarify modern WWAN reachability handling Make the modern ODWReachability WWAN path explicit so iOS 12+ builds unambiguously use the NWPathMonitor-backed state, while the legacy SCNetworkReachability fallback remains only for older Apple deployment targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index a24c4506e..f7d6aa158 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -642,20 +642,16 @@ -(BOOL)isReachable -(BOOL)isReachableViaWWAN { +#if TARGET_OS_IPHONE + BOOL modernWWANReachable = [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; #if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { -#if TARGET_OS_IPHONE - return [self awaitModernPathSnapshot] && - ODWModernPathIsReachable(self.currentPathStatus) && - self.currentPathUsesWWAN; -#else - return NO; -#endif + return modernWWANReachable; } -#if TARGET_OS_IPHONE - SCNetworkReachabilityFlags flags = 0; if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) @@ -670,19 +666,15 @@ -(BOOL)isReachableViaWWAN } } } -#endif return NO; #endif #if !ODW_LEGACY_REACHABILITY_REQUIRED -#if TARGET_OS_IPHONE - return [self awaitModernPathSnapshot] && - ODWModernPathIsReachable(self.currentPathStatus) && - self.currentPathUsesWWAN; + return modernWWANReachable; +#endif #else return NO; #endif -#endif } -(BOOL)isReachableViaWiFi From 366f87a7b18a4bc5139e123e9f3dca187a793ded Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 2 May 2026 16:34:04 -0500 Subject: [PATCH 06/12] Avoid blocking Apple reachability construction Modern reachability should not synchronously resolve DNS or reject the generic internet reachability address, and the path monitor context must not hold a stale raw owner pointer. Also avoid blocking the main thread while waiting for the first NWPathMonitor snapshot. Files changed: - third_party/Reachability/ODWReachability.m Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 41 ++++++++-------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index f7d6aa158..76a040648 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -32,7 +32,6 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import #import #import -#import NSString *const kNetworkReachabilityChangedNotification = @"NetworkReachabilityChangedNotification"; @@ -41,7 +40,11 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF @interface ODWReachabilityMonitorContext : NSObject -@property (nonatomic, assign) ODWReachability *owner; +#if __has_feature(objc_arc) +@property (nonatomic, weak) ODWReachability *owner; +#else +@property (nonatomic, unsafe_unretained) ODWReachability *owner; +#endif @end @@ -96,27 +99,6 @@ static BOOL ODWModernPathIsReachable(nw_path_status_t status) return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; } -static BOOL ODWHostResolves(NSString *hostname) -{ - if (hostname == nil || hostname.length == 0) - { - return NO; - } - - struct addrinfo hints = {}; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - struct addrinfo *result = NULL; - int lookupResult = getaddrinfo(hostname.UTF8String, NULL, &hints, &result); - if (result != NULL) - { - freeaddrinfo(result); - } - - return lookupResult == 0; -} - #if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) @@ -163,7 +145,7 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname formattedHostname = [NSString stringWithFormat:@"https://%@", hostname]; } NSURL *url = [NSURL URLWithString:formattedHostname]; - if (url == nil || url.host == nil || !ODWHostResolves(url.host)) + if (url == nil || url.host == nil) { NSLog(@"Invalid hostname"); return nil; @@ -206,8 +188,7 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress struct sockaddr_in *address = (struct sockaddr_in *)hostAddress; if (address->sin_addr.s_addr == INADDR_ANY) { - NSLog(@"Invalid address"); - return nil; + return [[self alloc] init]; } NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(address->sin_addr)]; @@ -386,6 +367,14 @@ -(BOOL)awaitModernPathSnapshot return NO; } + // Avoid blocking reachability queries on the main thread before the first + // NWPathMonitor update arrives. Callers get a conservative "unknown yet" + // result until the async update handler records the first snapshot. + if ([NSThread isMainThread]) + { + return NO; + } + long waitResult = dispatch_semaphore_wait( self.initialPathSemaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); From c13c90c35819070bd874aea4a0e25db092d06490 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 5 May 2026 11:11:24 -0500 Subject: [PATCH 07/12] Gate ODWReachability NWPathMonitor calls by availability Annotate the modern-path helpers (`ODWModernPathIsReachable`, `ensureModernPathMonitor`, `awaitModernPathSnapshot`, `handleModernPathUpdate:`, `notifyModernPathChange`) with `API_AVAILABLE(macos(10.14), ios(12.0))` so that compiling against an older deployment target no longer triggers `-Wunguarded-availability-new` errors under `-Werror`. Also fix a latent bug in `isReachableViaWWAN` where the modern-path snapshot was computed before the `if (@available(...))` guard. On macOS 10.10 / iOS 10.0 deployment targets running on a host that lacks NWPathMonitor, this would invoke unavailable Network framework APIs. The modern-path branch is now only taken inside the `@available` block on legacy builds, mirroring the structure used in `isReachableViaWiFi`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index 76a040648..bff596849 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -68,10 +68,10 @@ @interface ODWReachability () -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; --(BOOL)ensureModernPathMonitor; --(BOOL)awaitModernPathSnapshot; --(void)handleModernPathUpdate:(nw_path_t)path; --(void)notifyModernPathChange; +-(BOOL)ensureModernPathMonitor API_AVAILABLE(macos(10.14), ios(12.0)); +-(BOOL)awaitModernPathSnapshot API_AVAILABLE(macos(10.14), ios(12.0)); +-(void)handleModernPathUpdate:(nw_path_t)path API_AVAILABLE(macos(10.14), ios(12.0)); +-(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0)); @end @@ -94,7 +94,7 @@ -(void)notifyModernPathChange; (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } -static BOOL ODWModernPathIsReachable(nw_path_status_t status) +static BOOL ODWModernPathIsReachable(nw_path_status_t status) API_AVAILABLE(macos(10.14), ios(12.0)) { return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; } @@ -632,13 +632,12 @@ -(BOOL)isReachable -(BOOL)isReachableViaWWAN { #if TARGET_OS_IPHONE - BOOL modernWWANReachable = [self awaitModernPathSnapshot] && - ODWModernPathIsReachable(self.currentPathStatus) && - self.currentPathUsesWWAN; #if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { - return modernWWANReachable; + return [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; } SCNetworkReachabilityFlags flags = 0; @@ -657,9 +656,10 @@ -(BOOL)isReachableViaWWAN } return NO; -#endif -#if !ODW_LEGACY_REACHABILITY_REQUIRED - return modernWWANReachable; +#else + return [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; #endif #else return NO; From 09a73ab775b50e6d2474a26614431c7a7acb2f39 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 5 May 2026 19:23:39 -0500 Subject: [PATCH 08/12] Capture ODWReachability semaphore before signal/wait `-handleModernPathUpdate:` and `-awaitModernPathSnapshot` previously read `self.initialPathSemaphore` more than once across the nil-check and the matching `dispatch_semaphore_signal`/`dispatch_semaphore_wait` call. `-stopNotifier` clears the property from an arbitrary thread, so the second read could observe nil and pass it into libdispatch (which crashes on a nil semaphore). Capture the semaphore into a strong local once. Under ARC the local retains the dispatch_semaphore_t for the duration of the call, so a concurrent stop will no longer race the signal/wait. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index bff596849..c86217c7f 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -362,7 +362,11 @@ -(BOOL)awaitModernPathSnapshot return YES; } - if (self.initialPathSemaphore == nil) + // Capture the semaphore into a local so a concurrent -stopNotifier on + // another thread cannot release the property between the nil-check and + // the wait below. + dispatch_semaphore_t semaphore = self.initialPathSemaphore; + if (semaphore == nil) { return NO; } @@ -376,7 +380,7 @@ -(BOOL)awaitModernPathSnapshot } long waitResult = dispatch_semaphore_wait( - self.initialPathSemaphore, + semaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); return waitResult == 0 && self.hasObservedPath; } @@ -393,9 +397,16 @@ -(void)handleModernPathUpdate:(nw_path_t)path BOOL firstPath = !self.hasObservedPath; self.hasObservedPath = YES; - if (firstPath && self.initialPathSemaphore != nil) + if (firstPath) { - dispatch_semaphore_signal(self.initialPathSemaphore); + // Capture the semaphore into a local so a concurrent -stopNotifier + // on another thread cannot release the property between the + // nil-check and the signal below. + dispatch_semaphore_t semaphore = self.initialPathSemaphore; + if (semaphore != nil) + { + dispatch_semaphore_signal(semaphore); + } } if (self.reachabilityObject == self) From a5de13d35f325096ca68a7b58ec1bf3ddc10cef5 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 11 May 2026 13:03:22 -0500 Subject: [PATCH 09/12] Address reachability review comments Preserve host/address-specific reachability semantics with SCNetworkReachability while keeping Network.framework for general path monitoring. Avoid waiting for the first NWPathMonitor update on the monitor queue, and use platform-specific Apple availability gates. Files changed: - third_party/Reachability/ODWReachability.h - third_party/Reachability/ODWReachability.m Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.h | 27 +- third_party/Reachability/ODWReachability.m | 390 +++++++++++---------- 2 files changed, 236 insertions(+), 181 deletions(-) diff --git a/third_party/Reachability/ODWReachability.h b/third_party/Reachability/ODWReachability.h index 57f3702f9..9a63e42c9 100644 --- a/third_party/Reachability/ODWReachability.h +++ b/third_party/Reachability/ODWReachability.h @@ -44,14 +44,39 @@ extern NSString* const kNetworkReachabilityChangedNotification; // Older Apple deployment targets still need the legacy SCNetworkReachability // backend at runtime. Newer targets can compile directly to the modern path. -#if TARGET_OS_IPHONE +#ifndef TARGET_OS_IOS +#define TARGET_OS_IOS TARGET_OS_IPHONE +#endif +#ifndef TARGET_OS_MACCATALYST +#define TARGET_OS_MACCATALYST 0 +#endif +#ifndef TARGET_OS_TV +#define TARGET_OS_TV 0 +#endif +#ifndef TARGET_OS_WATCH +#define TARGET_OS_WATCH 0 +#endif +#ifndef __TVOS_12_0 +#define __TVOS_12_0 120000 +#endif +#ifndef __WATCHOS_5_0 +#define __WATCHOS_5_0 50000 +#endif + +#if TARGET_OS_IOS || TARGET_OS_MACCATALYST #define ODW_LEGACY_REACHABILITY_REQUIRED (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0) +#elif TARGET_OS_TV +#define ODW_LEGACY_REACHABILITY_REQUIRED (__TV_OS_VERSION_MIN_REQUIRED < __TVOS_12_0) +#elif TARGET_OS_WATCH +#define ODW_LEGACY_REACHABILITY_REQUIRED (__WATCH_OS_VERSION_MIN_REQUIRED < __WATCHOS_5_0) #elif TARGET_OS_OSX #define ODW_LEGACY_REACHABILITY_REQUIRED (__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_14) #else #define ODW_LEGACY_REACHABILITY_REQUIRED 0 #endif +#define ODW_REACHABILITY_HAS_WWAN (TARGET_OS_IOS || TARGET_OS_MACCATALYST) + typedef NS_ENUM(NSInteger, ODWNetworkStatus) { // Apple NetworkStatus Compatible Names. NotReachable = 0, diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index c86217c7f..da6fe9967 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -35,6 +35,7 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF NSString *const kNetworkReachabilityChangedNotification = @"NetworkReachabilityChangedNotification"; +static char ODWReachabilityQueueKey; @class ODWReachability; @@ -68,10 +69,13 @@ @interface ODWReachability () -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; --(BOOL)ensureModernPathMonitor API_AVAILABLE(macos(10.14), ios(12.0)); --(BOOL)awaitModernPathSnapshot API_AVAILABLE(macos(10.14), ios(12.0)); --(void)handleModernPathUpdate:(nw_path_t)path API_AVAILABLE(macos(10.14), ios(12.0)); --(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0)); +-(BOOL)getReachabilityFlags:(SCNetworkReachabilityFlags *)flags; +-(BOOL)startLegacyNotifier; +-(void)stopLegacyNotifier; +-(BOOL)ensureModernPathMonitor API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); +-(BOOL)awaitModernPathSnapshot API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); +-(void)handleModernPathUpdate:(nw_path_t)path API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); +-(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); @end @@ -79,7 +83,7 @@ -(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0)); static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags) { return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c", -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', #else 'X', @@ -94,12 +98,11 @@ -(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0)); (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } -static BOOL ODWModernPathIsReachable(nw_path_status_t status) API_AVAILABLE(macos(10.14), ios(12.0)) +static BOOL ODWModernPathIsReachable(nw_path_status_t status) API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)) { return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; } -#if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { @@ -114,7 +117,6 @@ static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkRea [reachability reachabilityChanged:flags]; } } -#endif @implementation ODWReachability @@ -125,94 +127,116 @@ @implementation ODWReachability +(ODWReachability*)reachabilityWithHostName:(NSString*)hostname { - if (hostname == nil || [hostname length] == 0) - { - NSLog(@"Invalid hostname"); - return nil; - } return [ODWReachability reachabilityWithHostname:hostname]; } +(instancetype)reachabilityWithHostname:(NSString*)hostname { -#if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (hostname == nil || [hostname length] == 0) { -#endif - // Use Network.framework reachability for macOS 10.14 or higher. - NSString *formattedHostname = hostname; - if (![formattedHostname hasPrefix:@"https://"] && ![formattedHostname hasPrefix:@"http://"]) { - formattedHostname = [NSString stringWithFormat:@"https://%@", hostname]; - } - NSURL *url = [NSURL URLWithString:formattedHostname]; - if (url == nil || url.host == nil) + NSLog(@"Invalid hostname '%@': hostname is empty", hostname); + return nil; + } + + NSString *reachabilityHost = hostname; + NSURL *url = nil; + NSURLComponents *components = [NSURLComponents componentsWithString:hostname]; + if ([components.scheme length] > 0) + { + if ([components.host length] == 0) { - NSLog(@"Invalid hostname"); + NSLog(@"Invalid hostname '%@': URL has no host", hostname); return nil; } - ODWReachability *reachabilityInstance = [[self alloc] init]; - reachabilityInstance.url = url; - return reachabilityInstance; -#if ODW_LEGACY_REACHABILITY_REQUIRED + reachabilityHost = components.host; + url = components.URL; + } + + if (url == nil) + { + url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", reachabilityHost]]; } - // Use SCNetworkReachability for macOS 10.14 or lower #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]); + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [reachabilityHost UTF8String]); #pragma clang diagnostic pop if (ref) { - id reachability = [[self alloc] initWithReachabilityRef:ref]; + ODWReachability *reachability = [[self alloc] initWithReachabilityRef:ref]; + reachability.url = url; return reachability; } + const char *errorString = SCErrorString(SCError()); + NSLog(@"Invalid hostname '%@': SCNetworkReachabilityCreateWithName failed for '%@' (%s)", + hostname, + reachabilityHost, + errorString != NULL ? errorString : "unknown error"); return nil; -#endif } +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress { - if (hostAddress == NULL) { - NSLog(@"Invalid address"); + if (hostAddress == NULL) + { + NSLog(@"Invalid address: address pointer is null"); return nil; } -#if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + struct sockaddr_storage addressStorage; + bzero(&addressStorage, sizeof(addressStorage)); + struct sockaddr *address = (struct sockaddr *)hostAddress; + NSURL *url = nil; + if (address->sa_family == AF_INET) { -#endif - // Use Network.framework reachability for macOS 10.14 or higher. - struct sockaddr_in *address = (struct sockaddr_in *)hostAddress; - if (address->sin_addr.s_addr == INADDR_ANY) + char addressString[INET_ADDRSTRLEN] = { 0 }; + struct sockaddr_in *ipv4Address = (struct sockaddr_in *)&addressStorage; + *ipv4Address = *(struct sockaddr_in *)hostAddress; + if (ipv4Address->sin_len == 0) { - return [[self alloc] init]; + ipv4Address->sin_len = sizeof(*ipv4Address); + } + address = (struct sockaddr *)ipv4Address; + if (inet_ntop(AF_INET, &ipv4Address->sin_addr, addressString, sizeof(addressString)) != NULL) + { + url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%s", addressString]]; } - - NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(address->sin_addr)]; - NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", addressString]]; - ODWReachability *reachabilityInstance = [[self alloc] init]; - reachabilityInstance.url = url; - return reachabilityInstance; -#if ODW_LEGACY_REACHABILITY_REQUIRED } - - // Use SCNetworkReachability for macOS 10.14 or lower + else if (address->sa_family == AF_INET6) + { + char addressString[INET6_ADDRSTRLEN] = { 0 }; + struct sockaddr_in6 *ipv6Address = (struct sockaddr_in6 *)&addressStorage; + *ipv6Address = *(struct sockaddr_in6 *)hostAddress; + if (ipv6Address->sin6_len == 0) + { + ipv6Address->sin6_len = sizeof(*ipv6Address); + } + address = (struct sockaddr *)ipv6Address; + if (inet_ntop(AF_INET6, &ipv6Address->sin6_addr, addressString, sizeof(addressString)) != NULL) + { + url = [NSURL URLWithString:[NSString stringWithFormat:@"https://[%s]", addressString]]; + } + } + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress); + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)address); #pragma clang diagnostic pop if (ref) { - id reachability = [[self alloc] initWithReachabilityRef:ref]; + ODWReachability *reachability = [[self alloc] initWithReachabilityRef:ref]; + reachability.url = url; return reachability; } + const char *errorString = SCErrorString(SCError()); + NSLog(@"Invalid address: SCNetworkReachabilityCreateWithAddress failed (%s)", + errorString != NULL ? errorString : "unknown error"); return nil; -#endif } +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:(NSError *)error url:(NSURL *)url @@ -245,10 +269,12 @@ +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:( +(ODWReachability *)reachabilityForInternetConnection { #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { return [[self alloc] init]; } +#else + return [[self alloc] init]; #endif struct sockaddr_in zeroAddress; @@ -262,12 +288,16 @@ +(ODWReachability *)reachabilityForInternetConnection +(ODWReachability*)reachabilityForLocalWiFi { #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { ODWReachability *reachability = [[self alloc] init]; reachability.monitorLocalWiFiOnly = YES; return reachability; } +#else + ODWReachability *reachability = [[self alloc] init]; + reachability.monitorLocalWiFiOnly = YES; + return reachability; #endif struct sockaddr_in localWifiAddress; @@ -290,6 +320,10 @@ -(instancetype)init { self.reachableOnWWAN = YES; self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); + dispatch_queue_set_specific(self.reachabilitySerialQueue, + &ODWReachabilityQueueKey, + &ODWReachabilityQueueKey, + NULL); self.currentPathStatus = nw_path_status_invalid; } @@ -378,6 +412,11 @@ -(BOOL)awaitModernPathSnapshot { return NO; } + // The update handler runs on this serial queue, so waiting here would deadlock it. + if (dispatch_get_specific(&ODWReachabilityQueueKey) == &ODWReachabilityQueueKey) + { + return NO; + } long waitResult = dispatch_semaphore_wait( semaphore, @@ -389,7 +428,7 @@ -(void)handleModernPathUpdate:(nw_path_t)path { self.currentPathStatus = nw_path_get_status(path); self.currentPathUsesWiFi = nw_path_uses_interface_type(path, nw_interface_type_wifi); -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN self.currentPathUsesWWAN = nw_path_uses_interface_type(path, nw_interface_type_cellular); #else self.currentPathUsesWWAN = NO; @@ -480,8 +519,13 @@ -(void)dealloc -(BOOL)startNotifier { + if (self.reachabilityRef != nil) + { + return [self startLegacyNotifier]; + } + #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { #endif // Use NWPathMonitor for macOS 10.14 or higher. @@ -497,9 +541,14 @@ -(BOOL)startNotifier return NO; #if ODW_LEGACY_REACHABILITY_REQUIRED } - - // Use SCNetworkReachability for macOS 10.14 or lower - // allow start notifier to be called multiple times + + return NO; +#endif +} + +-(BOOL)startLegacyNotifier +{ + // Allow start notifier to be called multiple times. if (self.reachabilityObject && (self.reachabilityObject == self)) { return YES; @@ -509,10 +558,12 @@ -(BOOL)startNotifier context.info = (__bridge void *)self; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context)) - { - if (SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue)) + BOOL callbackSet = SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context); + BOOL queueSet = callbackSet && SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue); #pragma clang diagnostic pop + if (callbackSet) + { + if (queueSet) { self.reachabilityObject = self; return YES; @@ -538,13 +589,18 @@ -(BOOL)startNotifier // if we get here we fail at the internet self.reachabilityObject = nil; return NO; -#endif } -(void)stopNotifier { + if (self.reachabilityRef != nil) + { + [self stopLegacyNotifier]; + return; + } + #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { #endif // Use NWPathMonitor for macOS 10.14 or higher. @@ -565,7 +621,12 @@ -(void)stopNotifier return; } - // Use SCNetworkReachability for macOS 10.14 or lower + return; +#endif +} + +-(void)stopLegacyNotifier +{ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // First stop, any callbacks! @@ -575,7 +636,6 @@ -(void)stopNotifier SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL); #pragma clang diagnostic pop self.reachabilityObject = nil; -#endif } @@ -601,7 +661,7 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags if( (flags & testcase) == testcase ) connectionUP = NO; -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN if(flags & kSCNetworkReachabilityFlagsIsWWAN) { // We're on 3G. @@ -616,61 +676,63 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags return connectionUP; } +-(BOOL)getReachabilityFlags:(SCNetworkReachabilityFlags *)flags +{ + if (self.reachabilityRef == nil) + { + return NO; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + BOOL result = SCNetworkReachabilityGetFlags(self.reachabilityRef, flags); +#pragma clang diagnostic pop + return result; +} + -(BOOL)isReachable { + if (self.reachabilityRef != nil) + { + SCNetworkReachabilityFlags flags; + return [self getReachabilityFlags:&flags] && [self isReachableWithFlags:flags]; + } + #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { #endif return [self awaitModernPathSnapshot] && ODWModernPathIsReachable(self.currentPathStatus); #if ODW_LEGACY_REACHABILITY_REQUIRED } - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) - return NO; -#pragma clang diagnostic pop - - return [self isReachableWithFlags:flags]; + return NO; #endif } -(BOOL)isReachableViaWWAN { -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN + if (self.reachabilityRef != nil) + { + SCNetworkReachabilityFlags flags = 0; + return [self getReachabilityFlags:&flags] && + (flags & kSCNetworkReachabilityFlagsReachable) && + (flags & kSCNetworkReachabilityFlagsIsWWAN); + } + #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { +#endif return [self awaitModernPathSnapshot] && ODWModernPathIsReachable(self.currentPathStatus) && self.currentPathUsesWWAN; - } - - SCNetworkReachabilityFlags flags = 0; - - if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) - { - // Check we're REACHABLE - if(flags & kSCNetworkReachabilityFlagsReachable) - { - // Now, check we're on WWAN - if(flags & kSCNetworkReachabilityFlagsIsWWAN) - { - return YES; - } - } +#if ODW_LEGACY_REACHABILITY_REQUIRED } return NO; -#else - return [self awaitModernPathSnapshot] && - ODWModernPathIsReachable(self.currentPathStatus) && - self.currentPathUsesWWAN; #endif #else return NO; @@ -679,15 +741,32 @@ -(BOOL)isReachableViaWWAN -(BOOL)isReachableViaWiFi { + if (self.reachabilityRef != nil) + { + SCNetworkReachabilityFlags flags = 0; + if ([self getReachabilityFlags:&flags] && (flags & kSCNetworkReachabilityFlagsReachable)) + { +#if ODW_REACHABILITY_HAS_WWAN + if (flags & kSCNetworkReachabilityFlagsIsWWAN) + { + return NO; + } +#endif + return YES; + } + + return NO; + } + #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { #endif if (![self awaitModernPathSnapshot] || !ODWModernPathIsReachable(self.currentPathStatus)) { return NO; } -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN if (self.monitorLocalWiFiOnly) { return self.currentPathUsesWiFi; @@ -699,28 +778,6 @@ -(BOOL)isReachableViaWiFi #endif #if ODW_LEGACY_REACHABILITY_REQUIRED } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags = 0; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop - { - // Check we're reachable - if((flags & kSCNetworkReachabilityFlagsReachable)) - { -#if TARGET_OS_IPHONE - // Check we're NOT on WWAN - if((flags & kSCNetworkReachabilityFlagsIsWWAN)) - { - return NO; - } -#endif - return YES; - } - } return NO; #endif @@ -736,25 +793,21 @@ -(BOOL)isConnectionRequired -(BOOL)connectionRequired { + if (self.reachabilityRef != nil) + { + SCNetworkReachabilityFlags flags; + return [self getReachabilityFlags:&flags] && + (flags & kSCNetworkReachabilityFlagsConnectionRequired); + } + #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { #endif return [self awaitModernPathSnapshot] && self.currentPathStatus == nw_path_status_satisfiable; #if ODW_LEGACY_REACHABILITY_REQUIRED } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop - { - return (flags & kSCNetworkReachabilityFlagsConnectionRequired); - } return NO; #endif @@ -764,56 +817,38 @@ -(BOOL)connectionRequired // Dynamic, on demand connection? -(BOOL)isConnectionOnDemand { -#if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (self.reachabilityRef != nil) { -#endif - return NO; -#if ODW_LEGACY_REACHABILITY_REQUIRED - } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; + SCNetworkReachabilityFlags flags; + if (![self getReachabilityFlags:&flags]) + { + return NO; + } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop - { return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && (flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand))); } return NO; -#endif } // Is user intervention required? -(BOOL)isInterventionRequired { -#if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (self.reachabilityRef != nil) { -#endif - return NO; -#if ODW_LEGACY_REACHABILITY_REQUIRED - } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; + SCNetworkReachabilityFlags flags; + if (![self getReachabilityFlags:&flags]) + { + return NO; + } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop - { return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && (flags & kSCNetworkReachabilityFlagsInterventionRequired)); } return NO; -#endif } @@ -827,7 +862,7 @@ -(ODWNetworkStatus)currentReachabilityStatus if([self isReachableViaWiFi]) return ReachableViaWiFi; -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN return ReachableViaWWAN; #endif } @@ -837,8 +872,14 @@ -(ODWNetworkStatus)currentReachabilityStatus -(SCNetworkReachabilityFlags)reachabilityFlags { + if (self.reachabilityRef != nil) + { + SCNetworkReachabilityFlags flags = 0; + return [self getReachabilityFlags:&flags] ? flags : 0; + } + #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, *)) + if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) { #endif if (![self awaitModernPathSnapshot]) @@ -855,7 +896,7 @@ -(SCNetworkReachabilityFlags)reachabilityFlags { flags |= kSCNetworkReachabilityFlagsConnectionRequired; } -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN if (self.currentPathUsesWWAN) { flags |= kSCNetworkReachabilityFlagsIsWWAN; @@ -865,17 +906,6 @@ -(SCNetworkReachabilityFlags)reachabilityFlags #if ODW_LEGACY_REACHABILITY_REQUIRED } - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags = 0; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop - { - return flags; - } - return 0; #endif } From 47114177afa5c06e03bd9fec70aa5ee4672e2857 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 11 May 2026 13:04:00 -0500 Subject: [PATCH 10/12] Avoid unguarded NWPath status init Leave the Network.framework status initialization in modern monitor setup so older Apple deployment-target builds do not touch an availability-gated symbol from init. Files changed: - third_party/Reachability/ODWReachability.m Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 1 - 1 file changed, 1 deletion(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index da6fe9967..aa1aa1b4c 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -324,7 +324,6 @@ -(instancetype)init &ODWReachabilityQueueKey, &ODWReachabilityQueueKey, NULL); - self.currentPathStatus = nw_path_status_invalid; } return self; From c9288d1b2db3445ce46aec0e10d83ba09e052d26 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 11 May 2026 14:06:40 -0500 Subject: [PATCH 11/12] Limit reachability gates to supported Apple targets Remove tvOS and watchOS deployment-target handling because the SDK does not support those targets; keep the availability gates focused on iOS and macOS. Files changed: - third_party/Reachability/ODWReachability.h - third_party/Reachability/ODWReachability.m Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.h | 25 +++---------------- third_party/Reachability/ODWReachability.m | 28 +++++++++++----------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/third_party/Reachability/ODWReachability.h b/third_party/Reachability/ODWReachability.h index 9a63e42c9..190d4b9d8 100644 --- a/third_party/Reachability/ODWReachability.h +++ b/third_party/Reachability/ODWReachability.h @@ -45,37 +45,18 @@ extern NSString* const kNetworkReachabilityChangedNotification; // Older Apple deployment targets still need the legacy SCNetworkReachability // backend at runtime. Newer targets can compile directly to the modern path. #ifndef TARGET_OS_IOS -#define TARGET_OS_IOS TARGET_OS_IPHONE -#endif -#ifndef TARGET_OS_MACCATALYST -#define TARGET_OS_MACCATALYST 0 -#endif -#ifndef TARGET_OS_TV -#define TARGET_OS_TV 0 -#endif -#ifndef TARGET_OS_WATCH -#define TARGET_OS_WATCH 0 -#endif -#ifndef __TVOS_12_0 -#define __TVOS_12_0 120000 -#endif -#ifndef __WATCHOS_5_0 -#define __WATCHOS_5_0 50000 +#define TARGET_OS_IOS 0 #endif -#if TARGET_OS_IOS || TARGET_OS_MACCATALYST +#if TARGET_OS_IOS #define ODW_LEGACY_REACHABILITY_REQUIRED (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0) -#elif TARGET_OS_TV -#define ODW_LEGACY_REACHABILITY_REQUIRED (__TV_OS_VERSION_MIN_REQUIRED < __TVOS_12_0) -#elif TARGET_OS_WATCH -#define ODW_LEGACY_REACHABILITY_REQUIRED (__WATCH_OS_VERSION_MIN_REQUIRED < __WATCHOS_5_0) #elif TARGET_OS_OSX #define ODW_LEGACY_REACHABILITY_REQUIRED (__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_14) #else #define ODW_LEGACY_REACHABILITY_REQUIRED 0 #endif -#define ODW_REACHABILITY_HAS_WWAN (TARGET_OS_IOS || TARGET_OS_MACCATALYST) +#define ODW_REACHABILITY_HAS_WWAN TARGET_OS_IOS typedef NS_ENUM(NSInteger, ODWNetworkStatus) { // Apple NetworkStatus Compatible Names. diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index aa1aa1b4c..0940cef46 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -72,10 +72,10 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; -(BOOL)getReachabilityFlags:(SCNetworkReachabilityFlags *)flags; -(BOOL)startLegacyNotifier; -(void)stopLegacyNotifier; --(BOOL)ensureModernPathMonitor API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); --(BOOL)awaitModernPathSnapshot API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); --(void)handleModernPathUpdate:(nw_path_t)path API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); --(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); +-(BOOL)ensureModernPathMonitor API_AVAILABLE(macos(10.14), ios(12.0)); +-(BOOL)awaitModernPathSnapshot API_AVAILABLE(macos(10.14), ios(12.0)); +-(void)handleModernPathUpdate:(nw_path_t)path API_AVAILABLE(macos(10.14), ios(12.0)); +-(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0)); @end @@ -98,7 +98,7 @@ -(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } -static BOOL ODWModernPathIsReachable(nw_path_status_t status) API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)) +static BOOL ODWModernPathIsReachable(nw_path_status_t status) API_AVAILABLE(macos(10.14), ios(12.0)) { return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; } @@ -269,7 +269,7 @@ +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:( +(ODWReachability *)reachabilityForInternetConnection { #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { return [[self alloc] init]; } @@ -288,7 +288,7 @@ +(ODWReachability *)reachabilityForInternetConnection +(ODWReachability*)reachabilityForLocalWiFi { #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { ODWReachability *reachability = [[self alloc] init]; reachability.monitorLocalWiFiOnly = YES; @@ -524,7 +524,7 @@ -(BOOL)startNotifier } #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { #endif // Use NWPathMonitor for macOS 10.14 or higher. @@ -599,7 +599,7 @@ -(void)stopNotifier } #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { #endif // Use NWPathMonitor for macOS 10.14 or higher. @@ -698,7 +698,7 @@ -(BOOL)isReachable } #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { #endif return [self awaitModernPathSnapshot] && ODWModernPathIsReachable(self.currentPathStatus); @@ -722,7 +722,7 @@ -(BOOL)isReachableViaWWAN } #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { #endif return [self awaitModernPathSnapshot] && @@ -758,7 +758,7 @@ -(BOOL)isReachableViaWiFi } #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { #endif if (![self awaitModernPathSnapshot] || !ODWModernPathIsReachable(self.currentPathStatus)) @@ -800,7 +800,7 @@ -(BOOL)connectionRequired } #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { #endif return [self awaitModernPathSnapshot] && @@ -878,7 +878,7 @@ -(SCNetworkReachabilityFlags)reachabilityFlags } #if ODW_LEGACY_REACHABILITY_REQUIRED - if (@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)) + if (@available(macOS 10.14, iOS 12.0, *)) { #endif if (![self awaitModernPathSnapshot]) From 511320c4a489a99d37e11c2f14ce88894fc638c3 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 11 May 2026 15:14:00 -0500 Subject: [PATCH 12/12] Validate +reachabilityWithAddress: inputs Reject the IPv4 unspecified wildcard (INADDR_ANY / 0.0.0.0), the IPv6 unspecified wildcard (in6addr_any / ::), and any sa_family that is not AF_INET or AF_INET6 from the public +reachabilityWithAddress: entry point. These are not routable host addresses, so creating an SC ref for them produces an ambiguous reachability probe rather than a per-host result. The legacy +reachabilityForInternetConnection fallback (only reached on deployment targets older than macOS 10.14 / iOS 12) still needs the 'probe any internet' 0.0.0.0 sentinel, so it now creates the SC ref inline and bypasses the public validator. Also document why hostname-based reachability still routes through SCNetworkReachabilityCreateWithName: NWPathMonitor has no public hostname-targeted monitoring API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 40 +++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index 0940cef46..ec50fafa7 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -158,6 +158,11 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", reachabilityHost]]; } + // NWPathMonitor has no public hostname-targeted API: it monitors the system + // network path, not per-host reachability. Hostname-based reachability still + // routes through SCNetworkReachabilityCreateWithName, with the deprecated-API + // warning locally suppressed. The modern path-monitor backend is used by the + // hostname-agnostic isReachable* methods below. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [reachabilityHost UTF8String]); @@ -199,6 +204,15 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress { ipv4Address->sin_len = sizeof(*ipv4Address); } + // Reject the unspecified IPv4 wildcard (INADDR_ANY / 0.0.0.0). It is not a + // routable host address; the SDK only uses it internally as the legacy + // "internet anywhere" SC probe, which goes through a private path that + // bypasses this validator. + if (ipv4Address->sin_addr.s_addr == htonl(INADDR_ANY)) + { + NSLog(@"Invalid address: IPv4 unspecified address (0.0.0.0) is not a valid host"); + return nil; + } address = (struct sockaddr *)ipv4Address; if (inet_ntop(AF_INET, &ipv4Address->sin_addr, addressString, sizeof(addressString)) != NULL) { @@ -214,12 +228,23 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress { ipv6Address->sin6_len = sizeof(*ipv6Address); } + // Reject the unspecified IPv6 wildcard (in6addr_any / ::), same reasoning as IPv4. + if (memcmp(&ipv6Address->sin6_addr, &in6addr_any, sizeof(struct in6_addr)) == 0) + { + NSLog(@"Invalid address: IPv6 unspecified address (::) is not a valid host"); + return nil; + } address = (struct sockaddr *)ipv6Address; if (inet_ntop(AF_INET6, &ipv6Address->sin6_addr, addressString, sizeof(addressString)) != NULL) { url = [NSURL URLWithString:[NSString stringWithFormat:@"https://[%s]", addressString]]; } } + else + { + NSLog(@"Invalid address: unsupported sa_family %d (expected AF_INET or AF_INET6)", address->sa_family); + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -277,12 +302,25 @@ +(ODWReachability *)reachabilityForInternetConnection return [[self alloc] init]; #endif + // Legacy SC fallback. Apple's reference Reachability uses the zero IPv4 + // address (INADDR_ANY) here as a "probe any internet" sentinel — the public + // +reachabilityWithAddress: now rejects that wildcard, so create the SC ref + // directly and bypass the validator. struct sockaddr_in zeroAddress; bzero(&zeroAddress, sizeof(zeroAddress)); zeroAddress.sin_len = sizeof(zeroAddress); zeroAddress.sin_family = AF_INET; - return [self reachabilityWithAddress:&zeroAddress]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress( + kCFAllocatorDefault, (const struct sockaddr*)&zeroAddress); +#pragma clang diagnostic pop + if (ref) + { + return [[self alloc] initWithReachabilityRef:ref]; + } + return nil; } +(ODWReachability*)reachabilityForLocalWiFi