NetService NutHouse

March 14, 2019

By Jeff Johnson

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 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. You can see my workaround in full context on GitHub. In short, I replaced this:

let txtRecord = NetService.dictionary(fromTXTRecord:txtRecordData)

with this:

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 CFNetServiceCreateDictionaryWithTXTData():

$ 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]

The function 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.