DispatchData ARC release crash? [Xcode 8b5]

Using the new Swift 3 libdispatch interface… but without much success.

Would appreciate if someone could proofread the following snippet and make sure I'm not doing anything stupid and confirm it's indeed a bug before I file a bug report. (Or explain what I'm doing wrong)


func crashesWhenDone()
{
    let s1 = StaticString(stringLiteral: "This is FOOBAR!")
    let data = s1.withUTF8Buffer {
        (b: UnsafeBufferPointer<UInt8>) -> DispatchData in  
        return DispatchData(bytes: b)
    }

    // EXC_BAD_ACCESS after this point.  I assume when ARC attempts to release `data`.
    // libswiftDispatch.dylib`partial apply forwarder for reabstraction thunk helper from @callee_unowned @convention(block) () -> () to @callee_owned () -> ():
}


Thank you!


EDIT: Going to re-type the code comment here because I've seen cases where these comments don't show:

The moment you leave the scope fo the function, there's a EXC_BAD_ACCESS crash which points to:

libswiftDispatch.dylib`partial apply forwarder for reabstraction thunk helper from @callee_unowned @convention(block) () -> () to @callee_owned () -> ():


Can anyone else reproduce this error? Thank you

Replies

Hmmm, weird. I can’t see anything obviously broken about your code (it’s weird lookin’, but that’s kinda normal when you’re trying to isolate a problem). And, when I put your code in a small project here in my office, it ran just fine. Specifically, I added this code:

import Dispatch

func crashesWhenDone() 
{ 
    let s1 = StaticString(stringLiteral: "This is FOOBAR!") 
    let data = s1.withUTF8Buffer { 
        (b: UnsafeBufferPointer<UInt8>) -> DispatchData in   
        return DispatchData(bytes: b) 
    } 
    print(data)
}

crashesWhenDone()

to a project created from the Command Line Tool template. When built with Xcode 8.0b5 and run on macOS 10.11.6, it printed this:

DispatchData(__wrapped: OS_dispatch_data)

I test both Debug and Release builds, just in case there’s some weird optimisation issue in play.

My recommendation: try with a newly created project, as I’ve described above, and see what you get.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for the reply. I'm on:

  • macOS 10.11.6 (15G31)
  • Xcode 8 beta 5 (8S193k)


I tried clearing the Derived Data, and created a new Swift Cocoa project which I only edited the "AppDelegate.swift" to read:


import Cocoa
import Dispatch
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet weak var window: NSWindow!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let s1 = StaticString(stringLiteral: "This is a foobar test")
        let d = s1.withUTF8Buffer {
            (b: UnsafeBufferPointer<UInt8>) -> DispatchData in
            print("utf8ToDispatchData() - B")
            return DispatchData(bytes: b)
        }
        print("test")
    }
  
    func applicationWillTerminate(_ aNotification: Notification) {
    }
}


Compiled and got the identical crash upon launching program.

I tried just what you said, and got the same crash. However, the backtrace of the crash is very strange:


Thread 8 Queue : com.apple.root.default-qos (concurrent)
#0 0x000000010057fe51 in partial apply for thunk ()
#1 0x0000000100892070 in _dispatch_call_block_and_release ()
#2 0x0000000100884cc5 in _dispatch_client_callout ()
#3 0x0000000100889457 in _dispatch_root_queue_drain ()
#4 0x00000001008888a5 in _dispatch_worker_thread3 ()
#5 0x00000001008e7336 in _pthread_wqthread ()
#6 0x00000001008e4f91 in start_wqthread ()
Enqueued from com.apple.main-thread (Thread 1)Queue : com.apple.main-thread (serial)
#0 0x000000010088ca6e in _dispatch_async_f_slow ()
#1 0x00000001008852cc in -[OS_dispatch_data dealloc] ()
#2 0x000000010000263b in AppDelegate.applicationDidFinishLaunching(Notification) -> () at …/AppDelegate.swift:14
#3 0x0000000100002908 in @objc AppDelegate.applicationDidFinishLaunching(Notification) -> () ()
…


So, the deallocation of the DispatchData on the main thread dispatched a block asynchronously to a global queue, and *that's* what crashed. So it sort of looks like a lifetime issue, perhaps not directly related to GCD. If I change your source code lines 6-8 as follows:


var d: DispatchData! = nil
func applicationDidFinishLaunching(_ aNotification: Notification) {
let s1 = StaticString(stringLiteral: "This is a foobar test")
d = s1.withUTF8Buffer {


there's no crash.


Maybe Quinn can reproduce the crash with this app structure and will figure out the problem.

First up, you should file a bug about this, including whatever information you think is relevant from the following. Please post your bug number, just for the record.

With your specific instructions (thanks!) I was able to reproduce the problem. I then simplified the code to get rid of a couple of potential pitfalls:

  • StaticString
    is kinda weird, so I replaced it with
    [UInt8]
  • I printed

    d
    , just in case throwing away the result triggered the problem

After that the final code looked like this:

func applicationDidFinishLaunching(_ aNotification: Notification) { 
    let a: [UInt8] = [1, 2, 3]
    let d = a.withUnsafeBufferPointer { (b: UnsafeBufferPointer<UInt8>) -> DispatchData in
        print("utf8ToDispatchData() - B") 
        return DispatchData(bytes: b) 
    } 
    print(d) 
}

and it still reproduced the problem. Here’s the backtrace:

Thread 2 Queue : com.apple.root.default-qos (concurrent)
#0 … partial apply for thunk ()
#1 … _dispatch_call_block_and_release ()
#2 … _dispatch_client_callout ()
#3 … _dispatch_root_queue_drain ()
#4 … _dispatch_worker_thread3 ()
#5 … _pthread_wqthread ()
#6 … start_wqthread ()
Enqueued from com.apple.main-thread (Thread 1)Queue : com.apple.main-thread (serial)
#0 … _dispatch_async_f_slow ()
#1 … -[OS_dispatch_data dealloc] ()
#2 … AppDelegate.applicationDidFinishLaunching(Notification) -> () at /Users/quinn/Desktop/xxb/xxb/AppDelegate.swift:16
…

The key thing is thread 1 frame 1. If you disassemble it you see this:

(lldb) disas -n "-[OS_dispatch_data dealloc]"
libdispatch.dylib`-[OS_dispatch_data dealloc]:
    0x1008812b5 <+0>:  pushq  %rbp
    0x1008812b6 <+1>:  movq  %rsp, %rbp
    0x1008812b9 <+4>:  pushq  %r15
    0x1008812bb <+6>:  pushq  %r14
    0x1008812bd <+8>:  pushq  %r12
    0x1008812bf <+10>:  pushq  %rbx
    0x1008812c0 <+11>:  subq  $0x10, %rsp
    0x1008812c4 <+15>:  movq  %rdi, %rbx
    0x1008812c7 <+18>:  callq  0x10088133a  ; _dispatch_data_dispose
    0x1008812cc <+23>:  movq  0x8(%rbx), %r12
    …

It’s this call to

_dispatch_data_dispose
that triggers the crash. Happily, that code is open source:
void
_dispatch_data_dispose(dispatch_data_t dd)
{
    if (_dispatch_data_leaf(dd)) {
        _dispatch_data_destroy_buffer(dd->buf, dd->size, dd->do_targetq,
                dd->destructor);
    } else {
        size_t i;
        for (i = 0; i < _dispatch_data_num_records(dd); ++i) {
            _dispatch_data_release(dd->records[i].data_object);
        }
        free((void *)dd->buf);
    }
}

I suspect (but didn’t confirm) that it’s running through the code path to

_dispatch_data_destroy_buffer
:
static void
_dispatch_data_destroy_buffer(const void* buffer, size_t size,
        dispatch_queue_t queue, dispatch_block_t destructor)
{
    if (destructor == DISPATCH_DATA_DESTRUCTOR_FREE) {
        free((void*)buffer);
    } else if (destructor == DISPATCH_DATA_DESTRUCTOR_NONE) {
        // do nothing
    } else if (destructor == DISPATCH_DATA_DESTRUCTOR_VM_DEALLOCATE) {
        mach_vm_size_t vm_size = size;
        mach_vm_address_t vm_addr = (uintptr_t)buffer;
        mach_vm_deallocate(mach_task_self(), vm_addr, vm_size);
    } else {
        if (!queue) {
            queue = dispatch_get_global_queue(
                    DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        }
        dispatch_async_f(queue, destructor, _dispatch_call_block_and_release);
    }
}

which is where the

dispatch_async_f
is coming from (thread 1 frame 0). But check out this code: it special cases each of the standard ways of disposing of the dispatch data’s underlying buffer (
DISPATCH_DATA_DESTRUCTOR_FREE
and so on), so the only way to get to the
dispatch_async_f
is if the dispatch data is using a custom deallocator. Presumably that custom deallocator is being supplied by the Swift overlay.

I didn’t investigate any further because a) it’s clear that this should work, b) there’s an obvious place to look for the bug, and c) that’s in a current beta component (Xcode 8.0b5’s Swift compiler and overlay).

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you both for confirming this is, indeed, a bug. 🙂


Bug submitted: rdar://27833415