Ping without CFSockets

All of our uses of CFSockets have started causing crashes in iOS 16. They seem to be deprecated so we are trying to transition over to using the Network framework and NWConnection to try to fix the crashes.

One of our uses of them is to ping a device on the local network to make sure it is there and online and provide a heartbeat status in logs as well as put the application into a disabled state if it is not available as it is critical to the functionality of the app. I know it is discouraged to disable any functionality based on the reachability of a resource but this is in an enterprise environment where the reachability of this device is mission critical.

I've seen other people ask about the ability to ping with the Network framework and the answers I've found have said that this is not possible and pointed people to the SimplePing sample code but it turns out our existing ping code is already using this technique and it is crashing just like our other CFSocket usages, inside CFSocketInvalidate with the error BUG IN CLIENT OF LIBPLATFORM: Trying to recursively lock an os_unfair_lock.

Is there any updated way to perform a ping without using the CFSocket APIs that now seem to be broken/unsupported on iOS 16?

Replies

They seem to be deprecated so we are trying to transition over to using the Network framework and NWConnection

Moving to Network framework is a good thing. CFSocket, despite the name, was never a good fit for networking.

Sadly, Network framework does not have a high-level ping API. If you’d like to see that added to the wish list, file an enhancement request along those lines. Please post your bug number, just for the record.

That means that you will have to write your own code for ping. When I wrote SimplePing, way back in the day, CFSocket was the best way to use a socket asynchronously. That’s not been the case for a while. If I were writing it again today, I’d use a Dispatch event source.

Having said that, CFSocket should still work and I’m surprised you’re seeing this:

it turns out our existing ping code is already using this technique and it is crashing … inside CFSocketInvalidate with the error BUG IN CLIENT OF LIBPLATFORM: Trying to recursively lock an os_unfair_lock.

Please post a crash report for that. See Posting a Crash Report for advice on how to do that.


Just as a data point I downloaded SimplePing and ran the MacTool target and it works just fine on my machine. The tool never calls CFSocketInvalidate though, so I then tweaked it to stop pinging after the fifth ping by adding this code to the end of -simplePing:didReceivePingResponsePacket:sequenceNumber::

if (sequenceNumber >= 4) {
    [self.pinger stop];
    self.pinger = nil;
    [self.sendTimer invalidate];
    self.sendTimer = nil;
}

I confirmed that does cause it to call CFSocketInvalidate and that doesn’t crash. This is on macOS 13.1. So, I’m not sure why you’re seeing crashers but AFAICT CFSocketInvalidate is fundamentally broken.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Okay, I submitted an enhancement request to provide a high-level ping API: FB11989640

AFAICT CFSocketInvalidate is fundamentally broken.

I'm guessing from context (since you are saying that it should work and did not crash for you) that you mean "is NOT fundamentally broken"?

The crash is not something that happens with every use of the API. I do not understand the rhyme or reason behind it. I have been able to reproduce it but not in any reliable or consistent way, I just run through the code that uses it over and over again and sometimes it will crash.

To give you a better picture of the crash, this is an enterprise kiosk application running in stores for customers to use and is running on ~5600 iPads, of which ~2000 are on iOS 16. We are seeing over 200 crashes per day from this same CFSocket stack trace inside CFSocketInvalidate. It is crashing ONLY on the iOS 16 iPads. There have been 0 instances of this crash on the other iPads that are mostly on iOS 15 with a few on iOS 14 and iOS 13. The crashes are split between our 3 different usages of CFSockets. The one that is the focus of this post is the SimplePing usage. We also use CFSockets to do socket communication with a card reader device and we are transitioning this use case to use NWConnection instead. The final usage is inside of GCDAsyncSocket inside a logging library which gets used for some logging to Splunk, we have not identified how we will replace that one yet. I only list these other usages in case their presence somehow informs the issue but I will restate that this code with all 3 of these usages does not crash prior to iOS 16 and we have been using them years.

I have attached a crash report from one of the crashes using SimplePing.

Also submitted a feedback report for this CFSocket crash: FB11989660

Thanks for the bug reports.

It’s hard to say for sure what’s going on here but I suspect that some other CFSocket or CFSocketStream code in your app is triggering this. I have two suggestions for moving forward here:

  • You could open a DTS tech support incident and I can take a much more detailed look at why your ping code is crashing in this way.

  • You could move your ping code over to a Dispatch source, which will definitely avoid this crash [1].

I’d probably lean towards the second option because the less CFSocket code you have in your app the better, but either one is fine (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Although it might not fix all your crashes. If, as I suspect, the root cause of this issues lies in your other CFSocket or CFSocketStream code, this crash may just move to some other site.

Are there any resources you can point me to for implementing a ping using Dispatch source? All of the stuff I can find online still uses CFSocket and most of it just straight up uses your SimplePing code. I don't know where I would even start so we may end up needing to go the TSI route (thank you for the information about that).

If you look at the SimplePing source you’ll see that it uses BSD Sockets for almost everything. It only uses CFSocket for the run loop integration [1]. So the vast bulk of this code stays the same; you only need to use Dispatch to replace that run loop integration.

And for that you need a Dispatch read source. Pasted in below is a small snippet that shows how you might approach this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] This was a deliberate design choice. CFSocket does some weird things under the covers and the only thing I trust it to do is run loop integration.


final class SocketMonitor {

    init(fd: FileDescriptor, queue: DispatchQueue) throws {
        self.queue = queue
        self.state = .initialised(fd: try fd.duplicate())
    }
    
    deinit {
        switch self.state {
        case .initialised(fd: let fd): try! fd.close()
        case .started(source: _): fatalError()
        case .cancelled: break
        }
    }
    
    let queue: DispatchQueue
    private enum State {
        case initialised(fd: FileDescriptor)
        case started(source: DispatchSourceRead)
        case cancelled
    }
    private var state: State
    
    func start() {
        dispatchPrecondition(condition: .onQueue(self.queue))
        // Redundant start is not OK, nor is starting after a cancel.
        guard case .initialised(let fd) = state else { fatalError() }
        let source = DispatchSource.makeReadSource(fileDescriptor: fd.rawValue, queue: self.queue)
        self.state = .started(source: source)
        source.setEventHandler() {
            do {
                print("will read")
                var buf = [UInt8](repeating: 0, count: 2048)
                let bytesRead = try buf.withUnsafeMutableBytes { try fd.read(into: $0) }
                print("did read, count: \(bytesRead)")
            } catch {
                print("did not read, error: \(error)")
            }
        }
        source.setCancelHandler() {
            try! fd.close()
        }
        source.activate()
    }
    
    func cancel() {
        dispatchPrecondition(condition: .onQueue(self.queue))
        // Redundant cancellation is OK.
        guard case .started(source: let source) = self.state else { return }
        source.cancel()
        self.state = .cancelled
    }
}

To follow up, we ended up focusing on moving our 2 non-ping CFSocket usages to the Network framework to get that rolled out before we started working on converting the ping code over to a dispatch source.

The interesting thing so far is that fixing those 2 seems to have fixed the ping crash as well. It has been about a week now with the fixes being out in prod and we haven't seen the ping crash once so far.

This leads me to believe that whatever broke it in iOS 16 only breaks it when there are multiple (concurrent?) usages of it throughout the app.