[Unsafe Pointers] Which is the expected behavior? Crash or not?

To begin with the conclusion, the following (experimental) code can be executed successfully on macOS whereas a run-time error happens on Linux.

protocol MyProtocol {
  var string: String { get set }
}

struct MyStruct: MyProtocol {
  var string: String = "Hello, World!"
}

/// An odd type-erasure.
/// This kind of type is sometimes helpful to communicate with C API.
class PointerWrapper {
  private class _PointerBox {
    func setString(_ string: String) { fatalError() }
  }

  private final class _SomePointer<T>: _PointerBox where T: MyProtocol {
    let base: UnsafeMutablePointer<T>
    init(_ base: UnsafeMutablePointer<T>) { self.base = base }
    override func setString(_ string: String) {
      base.pointee.string = string
    }
  }

  private let pointer: _PointerBox
  init<T>(_ pointer: UnsafeMutablePointer<T>) where T: MyProtocol {
    self.pointer = _SomePointer<T>(pointer)
  }

  func setString(_ string: String) {
    pointer.setString(string)
  }
}

func useEphemeralPointer<T>(_ object: inout T) where T: MyProtocol {
  withUnsafeMutablePointer(to: &object) {
    var wrapper = PointerWrapper($0)
    withUnsafeMutablePointer(to: &wrapper) {
      $0.pointee.setString("Ephemeral.")
    }
  }
}

func useAllocatedPointer<T>(_ object: inout T) where T: MyProtocol {
  let objPointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
  objPointer.pointee = object
  defer {
    object = objPointer.pointee
    objPointer.deallocate()
  }

  let wrapper = PointerWrapper(objPointer)
  let wrapperPointer = UnsafeMutablePointer<PointerWrapper>.allocate(capacity: 1)
  wrapperPointer.pointee = wrapper
  defer {
    wrapperPointer.deallocate()
  }

  wrapperPointer.pointee.setString("Allocated.")
}

var something = MyStruct()
print(something.string) // Prints "Hello, World!"

useEphemeralPointer(&something) // βœ… Successful on both macOS and Linux.
print(something.string) // Prints "Ephemeral."

useAllocatedPointer(&something) // ❌ On Linux: πŸ’£ Program crashed: Bad pointer dereference
print(something.string) // Prints "Allocated." (on macOS).

I think that func useEphemeralPointer and func useAllocatedPointer behaves in the same way (at least from the viewpoint of caller).
I mean I guess the run-time error on Linux seems buggy.
However I am probably missing something.

Then, my question is "Which is the expected behavior? Crash or not?"

Can you identify the exact line where the error occurs?

I am not an expert in this area, but one thing that jumps out is that you call deallocate on pointers that reference initialized objects, without either deinitializing the memory or setting the pointers to null.

1 Like

Thank you. That's a good question and I'm sorry that I didn't mention it.

The runtime error is induced by wrapperPointer.pointee = wrapper, which means the program crashes before the pointers are deallocated:

func useAllocatedPointer<T>(_ object: inout T) where T: MyProtocol {
  let objPointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
  objPointer.pointee = object

  let wrapper = PointerWrapper(objPointer)
  let wrapperPointer = UnsafeMutablePointer<PointerWrapper>.allocate(capacity: 1)
  wrapperPointer.pointee = wrapper // ❌ πŸ’£ Crashes here.
  // Even if `deallocate` lines are removed.
}

The issue is you need to use wrapperPointer.initialize(to: object) rather than wrapperPointer.pointee = for the initialisation.

.pointee = does an assignment operation, which means releasing the previously pointed-to value and then copying the new value. After allocation, wrapperPointer is pointing to uninitialised memory, and so any release operation will result in invalid behaviour (the crash you’re seeing).

4 Likes

Thank you so much! That would be the correct implementation.

If that is the case, then is the behavior on macOS kind of bug? (i.g. That may cause memory leak?)
I wonder where such discrepancy comes from.

The allocator on macOS is likely returning an empty (zeroed) allocation and the linux one is not (in that it has a non 0 dereference inside). Since the 0 pointer address is a no-op to release it means that won't crash whereas if the pointer contains some junk leftover from some other allocation the release of that previous value (what the .pointee = does) tries to decrement the retain count of that junk pointer and wanders off into a crash.

1 Like

I'm grateful for your explanation.
Now I feel enlightened.

As a digression, I remember I preferred calloc to malloc in the old days.