Yesterday I received a crash report for Bonjeff, my open source Swift app that shows you a live display of the Bonjour services published on your network. Since I'm (in)famous for my Objective-C advocacy, I wrote an open source app in Swift to demonstrate publicly that I can also write Swift. (Honestly, though, a Swift app has been more trouble than it's worth, but that's a blog post for another day.) The crash was in the CFNetwork function
NetService.dictionary(fromTXTRecord:), which corresponds to
+[NSNetService dictionaryFromTXTRecordData:] in Objective-C. The crash report stated that the problem was "value type is not bridged to Objective-C".
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
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. You can see my workaround in full context on GitHub. In short, I replaced this:
let txtRecord = NetService.dictionary(fromTXTRecord:txtRecordData)
let txtRecord = CFNetServiceCreateDictionaryWithTXTData(nil, txtRecordData as CFData)?.takeRetainedValue() as? Dictionary<String,Data>
How did I come up with my workaround? I looked at the disassembly for CFNetwork and saw that
+[NSNetService dictionaryFromTXTRecordData:] calls
$ otool -tV /System/Library/Frameworks/CFNetwork.framework/CFNetwork +[NSNetService dictionaryFromTXTRecordData:]: 00000000001be10d pushq %rbp 00000000001be10e movq %rsp, %rbp 00000000001be111 pushq %rbx 00000000001be112 pushq %rax 00000000001be113 movq %rdx, %rbx 00000000001be116 testq %rbx, %rbx 00000000001be119 jne 0x1be149 00000000001be11b movq 0x2e17a6(%rip), %rdi ## Objc class ref: _OBJC_CLASS_$_NSException 00000000001be122 movq 0x1ddfb7(%rip), %rax ## literal pool symbol address: _NSInvalidArgumentException 00000000001be129 movq (%rax), %rdx 00000000001be12c movq 0x2deab5(%rip), %rsi ## Objc selector ref: raise:format: 00000000001be133 leaq 0x2b634e(%rip), %rcx ## Objc cfstring ref: @"%@: cannot convert nil to a dictionary." 00000000001be13a leaq 0x2b6367(%rip), %r8 ## Objc cfstring ref: @"+[NSNetService dictionaryFromTXTRecordData:" 00000000001be141 xorl %eax, %eax 00000000001be143 callq *0x1de567(%rip) ## Objc message: +[NSException raise:format:] 00000000001be149 movq 0x1de130(%rip), %rax ## literal pool symbol address: _kCFAllocatorDefault 00000000001be150 movq (%rax), %rdi 00000000001be153 movq %rbx, %rsi 00000000001be156 callq _CFNetServiceCreateDictionaryWithTXTData 00000000001be15b movq 0x2dd7b6(%rip), %rsi ## Objc selector ref: autorelease 00000000001be162 movq %rax, %rdi 00000000001be165 addq $0x8, %rsp 00000000001be169 popq %rbx 00000000001be16a popq %rbp 00000000001be16b jmpq *0x1de53f(%rip) ## Objc message: -[%rdi autorelease]
CFNetServiceCreateDictionaryWithTXTData() is declared to return
__nullable CFDictionaryRef, which means that we can typecheck the dictionary values when passing them to Swift, thereby avoiding the crash.
It's unfortunate that the declarations don't match the implementations. In fact, the declarations don't even match each other! The return value of
CFNetServiceCreateDictionaryWithTXTData() is declared as nullable, but the return value of
+[NSNetService dictionaryFromTXTRecordData:] is declared as nonnull, even though we can see that the implementation of the latter method simply returns the same value as the former function. I suspect that mistakes were made when nullability annotations were added to the Objective-C framework headers. After all, the comments in the headers indicate that
+[NSNetService dictionaryFromTXTRecordData:] should have been nullable:
Returns an NSDictionary created from the provided NSData. The keys will be UTF8-encoded NSStrings. The values are NSDatas. The caller is responsible for interpreting these as types appropriate to the keys. If the NSData cannot be converted into an appropriate NSDictionary, this method will return nil.
I've released Bonjeff 1.0.7 with my fix for the bug. I've also filed a Radar… NOT! You can read The Sad State of Logging Bugs for Apple by Michael Tsai to see why not.