Help understanding runtime crash involving non-escaping closures

I had a surprising issue come up recently that I don’t totally understand, which is possibly related to escaping/non-escaping closures. Consider the following code, which will ‘chain’ together a bunch of methods such that they get called sequentially.

import Foundation
​
final class Chainer {
    var callees: [Callee]
​
    init(callees: [Callee]) {
        self.callees = callees
    }
​
    func performAndInvoke(_ invocation: () -> Void) {
        // if you wrap this implementation in a call to
        // `withoutActuallyEscaping(invocation) { invocation in ... }`
        // then it won't crash at runtime. why?
        var chainedInvocation = invocation
        for callee in callees.reversed() {
            let nextInvocation = chainedInvocation
            chainedInvocation = {
                callee.performAndInvoke(nextInvocation)
            }
        }
​
        chainedInvocation()
    }
}
​
final class Callee {
    var value: String
​
    init(value: String) {
        self.value = value
    }
​
    func performAndInvoke(_ invocation: () -> Void) {
        print("performing with value: \(value)")
        invocation()
    }
}
​
let c: [Callee] = [
    Callee(value: "one"),
    // commenting out the second element prevents a runtime crash. why?
    Callee(value: "two"),
]
​
let ch = Chainer(callees: c)
ch.performAndInvoke({ print("final invocation") })

When I compile and run this snippet from the command line, I get a runtime crash with the error terminated by signal SIGBUS (Misaligned address error). If I compile with the undefined behavior sanitizer (swiftc -sanitize=undefined ... ) and run it, I get slightly more info, but it’s still not super-clear to me what the underlying problem is:

==22622==ERROR: UndefinedBehaviorSanitizer: BUS on unknown address (pc 0x000199ea9f5c bp 0x000199eea5e4 sp 0x00016b9a1cd0 T16579874)
==22622==The signal is caused by a UNKNOWN memory access.
==22622==Hint: this fault was caused by a dereference of a high value address (see register values below). Disassemble the provided pc to learn which register was used.

If I wrap the implementation within a call to withoutActuallyEscaping, it seems to fix the issue, but I’m not entirely sure why. It’s a bit surprising to me that this compiles successfully but seems to cause invalid memory access at runtime. Any ideas what the underlying issue is in this instance? Thanks in advance!

2 Likes

It does look like a bug as if I step through the app - it works, and if I run it - it crashes. The bug disappears when I put "escaping" for the closure parameter (even if the call is not actually escaping).

Compiler bug aside: the app would crash on stack overflow if you pass it a long list of items (like 50K), so proceed with this approach with care.

Edit: The following simpler version seems to be doing the same as your version, just without recursion / call chaining, so it won't blow stack up when passed a long list of items:

    func performAndInvoke(_ invocation: () -> Void) {
        for callee in callees {
            callee.performAndInvoke {}
        }
        invocation()
    }

I'd optimise it making the closure parameter of "callee.performAndInvoke" optional.