NWPath thread safety

Hello!


We use `NWPathMonitor` within our SDK to read the network connection details. Recently, we discovered a crash which makes me considering the thread safety of `NWPath` struct. In following code, the program passed the line `12`, but crashed on line `13`:


import Network


@available(iOS 12, *)
extension NWPathMonitor {
    var current: NetworkConnectionInfo {
        let info = currentPath

        return NetworkConnectionInfo(
            reachability: NetworkConnectionInfo.Reachability(from: info.status),
            availableInterfaces: Array(fromInterfaceTypes: info.availableInterfaces.map { $0.type }),
            supportsIPv4: info.supportsIPv4, // <-  passed
            supportsIPv6: info.supportsIPv6, // <-  crashed
            isExpensive: info.isExpensive
        )
    }
}


To my best understanding, something did happen concurrently between the current thread advanced from line `12` to `13`. Thus, I consider thread safety issue around `NWPath` returned by the `currentPath`. What I can't understand however, is why this crashes at all, given that `NWPath` is a struct, so its immutable value captured on `let info` should be thread safe.


Crash details:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000020
VM Region Info: 0x20 is not in any region.  Bytes before following region: 4369170400
      REGION TYPE                      START - END             [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
      UNUSED SPACE AT START
--->  
      __TEXT                 00000001046c4000-0000000104ee8000 [ 8336K] r-x/r-x SM=COW  ...g.app/Datadog
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [18544]


Thread 10 name:  Dispatch queue: com.datadoghq.ios-sdk-logs-upload
Thread 10 Crashed:
0   libswiftCore.dylib              0x00000001ba2cf978 swift_unknownObjectRelease + 16
1   Datadog                         0x0000000106508694 0x1064bc000 + 312980
2   Datadog                         0x0000000106506e78 0x1064bc000 + 306808
3   Datadog                         0x00000001065069b8 0x1064bc000 + 305592
4   Datadog                         0x0000000106507618 0x1064bc000 + 308760
5   Datadog                         0x00000001064cbfa8 0x1064bc000 + 65448
6   Datadog                         0x00000001064d133c 0x1064bc000 + 86844
7   Datadog                         0x00000001064d251c 0x1064bc000 + 91420
8   libdispatch.dylib               0x00000001ac41033c _dispatch_client_callout + 20


Note: we discovered this crash on iOS13.x device and couldn't manage to reproduce more times. In the crashing version of SDK, we do pass following `queue` to synchronize `NWPathMonitor` updates:

let queue = DispatchQueue(
    label: "com.datadoghq.network-connection-info",
    qos: .utility,
    attributes: [],
    target: DispatchQueue.global(qos: .utility)
)

It's hard to say exactly what is happening. My advice would be to start from a very standard case of path monitoring, providing updates on the main queue, and work your way into your current situation. For example, start with the basic case to see if you still get a crash:


let nwPathMonitor = NWPathMonitor()  
nwPathMonitor.pathUpdateHandler = { path in  
     
    if path.usesInterfaceType(.wifi) {  
        // Correctly goes to Wi-Fi via Access Point or Phone enabled hotspot  
        os_log("Path is Wi-Fi")  
    } else if path.usesInterfaceType(.cellular) {  
        os_log("Path is Cellular")  
    } else if path.usesInterfaceType(.wiredEthernet) {  
        os_log("Path is Wired Ethernet")  
    } else if path.usesInterfaceType(.loopback) {  
        os_log("Path is Loopback")  
    } else if path.usesInterfaceType(.other) {  
        os_log("Path is other")  
    }  
}  
nwPathMonitor.start(queue: .main) 


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Hey Matt, thanks for the reply. Indeed, we changed from the pulling model of `pathMonitor.currentPath` to receiving updates through `pathUpdateHandler` and customers of our SDK stopped reporting the issue. I think it's ultimately gone 😉.

Excellent. Thanks for updating the thread with the follow up.


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Update: When running the basic example with the following

let nwPathMonitor = NWPathMonitor()  

nwPathMonitor.pathUpdateHandler = { path in  

    if path.usesInterfaceType(.wifi) {  

         // Correctly goes to Wi-Fi via Access Point or Phone enabled hotspot  

        os_log("Path is Wi-Fi")  

    } else if path.usesInterfaceType(.cellular) {  

        os_log("Path is Cellular")  

    } else if path.usesInterfaceType(.wiredEthernet) {  

        os_log("Path is Wired Ethernet")  

    } else if path.usesInterfaceType(.loopback) {  

        os_log("Path is Loopback")  

    } else if path.usesInterfaceType(.other) {  

        os_log("Path is other")  

    }  

}  

let queue = DispatchQueue(label: "Monitor")

nwPathMonitor.start(queue: queue)

application crash on real device (use iphone 11 with ios 17.4.1)

btw on sim it works fine, only when I changes to .main as Matt Eaton suggested it work on device

NWPath thread safety
 
 
Q