Thursday, March 14, 2019

NetService NutHouse

Jeff Johnson:

I couldn’t reproduce the crash myself, so I did a bit of searching on the web, and I discovered the explanation on Stack Overflow. The function NetService.dictionary(fromTXTRecord:) is declared to return [String:Data], but when the TXT Record does not have the proper key=value format, CFNetwork inserts kCFNull in the dictionary where Data is expected. This causes Swift to crash.

In Objective-C, the method +[NSNetService dictionaryFromTXTRecordData:] does not crash. However, it still behaves badly, because it is declared to return NSDictionary<NSString*,NSData*>*, but the dictionary can contain NSNull instead of NSData, so your app could still crash if the code trusts the compiler and calls NSData methods on NSNull.

Needless to say, this bug is awful. It affects both macOS and iOS. Moreover, the bug has existed for more than two years, which is even more awful. Fortunately, developers can work around the bug. In Objective-C a workaround is easy, because we can just check the returned dictionary for NSNull values at runtime. In Swift, however, the crash occurs before we can check the dictionary, so we need to try something else.

He worked around it using the Core Foundation version of the method, which has a different signature and so bridges differently. But it seems to also work to disable the bridging by casting with as NSDictionary.

Previously: Swift Subclass of NSTextStorage Is Slow Because of Swift Bridging.

8 Comments RSS · Twitter

Michael, have you tried disabling the bridging successfully? This still crashes for me, but I'm not sure whether you mean something different:

let txtRecord = NetService.dictionary(fromTXTRecord:txtRecordData) as NSDictionary

@Jeff Yes, I tried this in a playground:

let txtData = NetService.data(fromTXTRecord: ["foo": Data()])
let dict = NetService.dictionary(fromTXTRecord: txtData) as NSDictionary

It crashes without the cast. With the cast, it doesn’t crash, and the result successfully casts to [String: NSNull]. I’d be interested to know what test data you’re using.

It took me a while to figure out, but the actual data from NetService is not exactly what you expect:

let bytes:[UInt8] = [3, 102, 111, 111]
if let stringFromBytes = String(data: Data(bytes: bytes), encoding: .utf8) {
    let txtData = NetService.data(fromTXTRecord: [stringFromBytes: Data()])
    let dict = NetService.dictionary(fromTXTRecord: txtData) as NSDictionary
    print(dict)
}
$ dns-sd -R 'My Service Name' _myservice._tcp local 4567 foo

Correction: The problem in my last code was that txtData was 0 bytes, and NetService.dictionary(fromTXTRecord: txtData) crashes with empty data. In contrast, CFNetServiceCreateDictionaryWithTXTData returns nil with empty data. This is still a problem, because NetService.dictionary(fromTXTRecord:) is supposed to be nonnull.

So you are correct, NetService.dictionary(fromTXTRecord: txtData) as NSDictionary doesn't crash, as long as you guard against giving it 0 bytes of data.

So to clarify, the test should be something like this:

let bytes:[UInt8] = [3, 102, 111, 111]
let dict = NetService.dictionary(fromTXTRecord: Data(bytes: bytes)) as NSDictionary

which doesn't crash.

@Jeff It looks like if you give it empty data, it returns nil. So you can just use:

let dict = NetService.dictionary(fromTXTRecord: Data()) as NSDictionary?

to not crash.

Yes, that would work. All of the crashing is because Swift trusts the Objective-C annotations, but in this case the Objective-C annotations for both generics and nullability fail to match the implementation.

Gluing a dynamic system onto another system is doable. Gluing a static system onto a dynamic system is endless stream of pain points both large and small that no amount of snake oil will cure.

Swift is the hole into which Apple has dug Cocoa. The least worst thing they could do now is hurry up and finish burying it. The sooner old messy bridged Cocoa APIs are superseded by modern clean Swift APIs that play to the strengths of Swift instead of the weaknesses of its ObjC bridge, the sooner we can all get back to the slightly more important business of actually making and selling competitive product.

(See also: the Python 2 to 3 migration debacle; the Java-Cocoa bridge; every tail that wagged the dog ever.)

Leave a Comment