Use-after-free involving non-escaping closures

Years ago, I asked about this problem when "chaining" closures together. I now know slightly more about what goes on in the compiler, perhaps barely enough to ask a more precise question, which is: how should the closure lifetime fixup pass handle cases like this?

Here is an example of the issue (and a corresponding bug report):

func test(_ closure: () -> Void) {
    var next = closure

    for i in 1...2 {
        let previous = next
        next = {
            print("iter: \(i)")
            previous()
        }
    }

    next()
}

test { print(42) }

If you run this program, it will typically crash with an invalid memory access. IIUC this has to do with the fact that the closures involved here are non-escaping, and non-escaping closures do not retain their context. The "closure lifetime fixup" pass is supposed to solve this problem, but in cases like the above, where there is a loop which re-assigns a closure variable that itself captures a previous non-escaping closure, it does not appear to work properly.

I've not yet looked too closely into what goes on in that pass, but was wondering if anyone familiar with it has ideas as to what could be done to fix this, or could shed any more light on the failure mode here.

4 Likes

My initial reaction would be that mutable local variables of nonescaping closure type shouldn't be allowed with the implementation we have today. The limitations we impose on nonescaping closures are not sophisticated enough to understand mutations of closure bindings themselves. I suspect you could also use this sort of mutation to break exclusivity of mutable captures, for instance.

4 Likes

Yeah, looks like @jamieQ found a soundness hole. The following program does not diagnose an exclusivity violation, static or dynamic:

func addr(_ x: C) -> String {
  return String(unsafeBitCast(x, to: UInt.self), radix: 16)
}

class C {
  func m(_ fn: () -> ()) {
    print("self is \(addr(self))")
    fn()
    print("self is \(addr(self))")
  }

  deinit {
    print("deinit of \(addr(self))")
  }
}

func h(_ x: inout C, _ fn: () -> ()) {
  x.m(fn)
}

func g(_ x: inout C, _ b: Bool, _ fn: () -> ()) {
  // error: overlapping accesses to 'x', but modification requires exclusive access
/*
  h(&x) {
    x = C()
  }
*/

  // ok!
  var fn = fn
  if b {
    fn = {
      x = C()
    }
  }

  h(&x, fn)
}

var x = C()
g(&x, true, {})

When I build it with -O and run it, I get this output which demonstrates something is wrong:

self is 104989ac0
deinit of 104989ac0
self is 104989ac0

Static exclusivity analysis depends on the ability to trace back each value of nonescaping function type either to an input parameter of the current function, or a specific partial_apply of a named function.

If you assign to a var in a conditional branch, then of course this problem is undecidable.

So we have to be conservative here, and either ban var with nonescaping function type entirely, or somehow extend the analysis to "merge regions" here when you assign to a var.

5 Likes