Why must a pointer obtained from an array or withUnsafe… never be stored, and how do you catch a dangling one?

3.9.5 · L5 · Learning

Predict first

Compile this with Address Sanitizer and run it. What does the sanitizer report?

// === main.swift ===
var saved: UnsafePointer<Int>?
func capture(_ p: UnsafePointer<Int>) { saved = p }
var arr = [Int](repeating: 7, count: 8)
capture(arr)                  // legal call; the pointer is valid only during it
arr.reserveCapacity(1000)     // moves the elements and frees the old buffer
print("reading the saved pointer...")
print(saved![0])
  • No report; it prints 7
  • A heap-use-after-free read of size 8
  • A stack-buffer-overflow

Mental model. A pointer from withUnsafe… is a day-pass to a building: fine inside the visit, but the room it opens may be rebuilt or demolished the moment you leave.

Passing arr to an UnsafePointer<Int> parameter, or calling withUnsafeBufferPointer, gives you the address of the array’s current buffer, valid only for the duration of that call. The language gives no promise that the buffer stays put or alive afterwards: growing, reserveCapacity, a copy-on-write copy or the array being released all move or free it. In the check the pointer is saved in a global and then reserveCapacity(1000) reallocates; reading the saved pointer is undefined behaviour. Without tooling it might print 7 for years and crash later in another build; the same applies to withCString, &value arguments, withUnsafeMutablePointer(to:) and strings, and to a pointer to an object (Unmanaged.toOpaque()) whose last strong reference went away.

The cure is structural: finish the work inside the closure, copy the data out (Array(buffer)), or allocate memory you own (UnsafeMutablePointer<Int>.allocate, deallocated by you) when C needs a stable address. To catch a dangling pointer, run with Address Sanitizer (-sanitize=address, the scheme option in Xcode): the check’s read is reported as heap-use-after-free with the allocation and free stacks. ASan costs memory and speed, and it only reports what actually executes, so it complements review (Q458 lists what each sanitizer catches). Keep an object alive across a call that uses a raw pointer to it with withExtendedLifetime.

reserveCapacity moved the elements and freed the old buffer; the saved pointer dangles.

Common traps

  • Caching the base address of an array between calls into C. Any mutation of the array may move it.
  • Believing “it worked in testing” means it is safe. Freed memory often still holds the old value.

They might follow up with

  • Does Unmanaged.passRetained solve it for objects? It keeps the object alive until you release; the pointer you hand to C stays valid for that period.
  • Is it the same for Data? Yes: withUnsafeBytes is valid only inside the closure.

Related questions

  • 3.6.3 What is Unmanaged, and what do takeRetainedValue and takeUnretainedValue mean?
  • 3.7.2 How do you find a retain cycle with the Memory Graph debugger?
  • 23.3.1 What do the Address, Thread and Undefined Behavior sanitizers catch, what do they cost, and when do you use…

← Can a reference be weak let, or unowned and optional, and what does…