How can I demonstrate a zombie object in Swift?
Asked Answered
P

4

6

I've read How to demonstrate memory leak and zombie objects in Xcode Instruments? but that's for objective-c. The steps don't apply.

From reading here I've understood zombies are objects which are:

  • deallocated
  • but something pointer is still trying to point to them and send messages to them.

not exactly sure how that's different from accessing a deallocated object.

I mean in Swift you can do:

var person : Person? = Person(name: "John")
person = nil
print(person!.name)

Is person deallocated? Yes!

Are we trying to point to it? Yes!

So can someone share the most common mistake which leads to creating a dangling pointer?

Pearcy answered 29/10, 2018 at 16:15 Comment(5)
I wouldn't worry about that in Swift. Just make sure you don't use force-unwrapped optionals (I only ever use them for IBOutlets) and you won't have a problem.Attaway
That's exactly what I thought. Does this here apply: say you have a cache whose entries are instances of NSData that were downloaded from some URL where the URL contains a session ID in the URL and that session ID + URL are used as the key to look up stuff in the cache. Now, say the user logs out, causing the session ID to be destroyed. If the cache isn't also pruned of all entries specific to that session ID, then all of those NSData objects will be abandonedPearcy
Note that the Swift example you give isn't an example of a dangling pointer – you're setting the reference to nil, meaning that you no longer have a reference to the object, regardless of whether it's still allocated. Perhaps the simplest example of obtaining an dangling pointer in Swift is with Unmanaged, e.g class C {}; var c = C(); Unmanaged.passUnretained(c).release(). c is now a dangling pointer. This isn't a "common mistake" though – and you should never be able to obtain a dangling pointer in Swift without dipping into such unsafe constructs (because Swift is a safe by default).Smelser
That said, there is currently a footgun without temporary pointer conversions that can create dangling pointers, e.g let ptr = UnsafePointer([1, 2, 3])ptr is a dangling pointer as the array-to-pointer conversion produces a pointer only valid for the duration of the call. Hoping to warn (and eventually error) on such conversions in github.com/apple/swift/pull/20070.Smelser
Oops: *with temporary pointer conversionsSmelser
S
6

This is not a dangling pointer or a zombie. When you use ! you're saying "if this is nil, then crash." You should not think of person as a pointer in Swift. It's a value. That value may be .some(T) or it may be .none (also called nil). Neither of those is dangling. They're just two different explicit values. Swift's nil is nothing like null pointers in other languages. It only crashes like null pointers when you explicitly ask it to.

To create zombies, you'll need to be using something like Unmanaged. This is extremely uncommon in Swift.

Shari answered 29/10, 2018 at 16:30 Comment(12)
From experience I know it's not a zombie. But based on what I'm reading, it fits the description ie. 1. It's deallocated 2. I'm pointing to it. So what am I missing?Pearcy
You're not pointing to it. There is no pointer in your code above. Swift pointers all have Unsafe in their names.Shari
I understand your (edited) answer now. But not your comment. If Person is a class type then isn't be pointed to using a pointer? Seems I'm not understanding something very foundational. Let me look more into UnsafePearcy
No. It's a strong reference. That's not the same thing as a pointer. See particularly the docs on UnsafePointer for the differences. Pointers are a low-level concept in Swift and not needed for most code. Most code deals in values and references.Shari
I don't understand the distinction. When I hear 'reference' I think of a pointer. Apparently I'm wrong. I'll look into the docs on UnsafePointer. ThanksPearcy
@Honey References are implemented by pointers underneath, but then again, so are all branching statements (function calls, return, if/else), arrays, closures and many other entities. Pointers underpin many things, that doesn't mean those things are equivalent to pointers.Tele
@Honey In every case where a pointer underpins the implementation of one of the higher level abstractions I mentioned, there are some mechanisms implemented that distinguish the behavior of the abstraction from the behavior of naive pointer use. For example, a strong reference can never be a dangling reference. That's because a strong reference has special behavior that ensures that objects it point to are kept alive (by contributing to their non-zero retain count).Tele
@Honey On the other hand, naive pointer use can cause dangling references, because the thing they point to has no idea that its lifetime is being relied upon by an external entity (the pointer). Hyperlinks have the same issue. When I link to foo.com, The web server at foo.com has no idea that I made a link to it here. The server owner could shutdown the server at any point, breaking my foo.com link. That's the analog of a dangling pointer in the HTTP world. Now imagine if I had a hyperlink, but also e-mailed the owner, telling him I depend on his service, asking him to keep it up.Tele
@Honey He would have a "customer count" (analog to retain count), which he would see is non-zero, so he would courteously keep his server running to serve his customers, keeping our links from breaking. That's the analog of a strong reference.Tele
@Honey The key distinction between my "notify the service owner before starting to link to their service, and notify them after stopping my linking" and "just blindly link" is the distinction between strong refs and raw pointers. Notice that my scheme is still underpinned by hyperlinks, but with more constraints.Tele
@Tele Thanks a lot. Just to be crystal clear. By strong ref you mean var name: String. By raw pointer you mean: var myInt: UnsafePointer<Int32>! ?Pearcy
@Honey String is a struct, so it's directly stored inline, and not by pointer/reference. But something like class C {}; let c = C() is what I mean by a strong ref. And you're correct for pointer, although the ! isn't relevant.Tele
I
6

Here's zombie attack in under 15 lines of code:

class Parent { }

class Child {
    unowned var parent: Parent // every child needs a parent

    init(parent: Parent) {
        self.parent = parent
    }
}

var parent: Parent? = Parent()
let child = Child(parent: parent!) // let's pretend the forced unwrap didn't happen
parent = nil // let's deallocate this bad parent
print(child.parent) // BOOM!!!, crash

What happens in this code is that Child holds an unowned reference to Parent, which becomes invalid once Parent gets deallocated. The reference holds a pointer to the no longer living parent (RIP), and accessing that causes a crash with a message similar to this:

Fatal error: Attempted to read an unowned reference but object 0x1018362d0 was already deallocated2018-10-29 20:18:39.423114+0200 MyApp[35825:611433] Fatal error: Attempted to read an unowned reference but object 0x1018362d0 was already deallocated

Note The code won't work in a Playground, you need a regular app for this.

Intercellular answered 29/10, 2018 at 18:22 Comment(4)
Technically speaking, that’s not a dangling pointer as Swift keeps track of unowned references in order to ensure it always traps on attempting to access it after all the strong references have gone. You can use unowned(unsafe) in order to remove this safety net and get an actual dangling pointer which yields undefined behaviour for an access after deallocation.Smelser
@Smelser fair point, removed the "dangling pointer" reference.Intercellular
It may also be confusing to call this a "zombie" since it won't show up in anything zombie-related in the Cocoa ecosystem. It's a Swift-specific thing, an unowned reference, rather than a dangling pointer. But I think this adds a lot to the discussion (pointing out that there is a Swift-specific thing that can cause a memory-related spooky-crash-at-a-distance without Unsafe), as long as readers aren't led to believe that this has anything to do with NSZombie.Shari
also here you won't get the EXC_BAD_ACCESS that comes out of the same principle but doesn't let you know who is the parent, when it go deallocated, who is the child and when it tried to access the parent. This is the dramatic part of this problem. but great explanationJobyna
P
6

Zombie objects are Objective-C objects which have been deallocated, but still receive messages.

In Objective-C, __unsafe_unretained can be used to create an additional pointer to an object which does not increase the reference count. In Swift that would be unowned(unsafe).

Here is a self-contained example:

import Foundation

class MyClass: NSObject {
    func foo() { print("foo"); }

    deinit { print("deinit") }
}

unowned(unsafe) let obj2: MyClass
do {
    let obj1 = MyClass()
    obj2 = obj1
    print("exit scope")
}
obj2.foo()

obj1 is a pointer to a newly created object, and obj2 is another pointer to the same object, but without increasing the reference counter. When the do { ... } block is left, the object is deallocated. Sending a message to it via obj2 causes the Zombie error:

exit scope
deinit
*** -[ZombieTest.MyClass retain]: message sent to deallocated instance 0x1005748d0

This can also happen if you use Managed (e.g. to convert pointers to Swift objects to C void pointers in order to pass them to C callback functions) and you don't choose the correct retained/unretained combination. A contrived example is:

let obj2: MyClass
do {
    let obj1 = MyClass()
    obj2 = Unmanaged.passUnretained(obj1).takeRetainedValue()
    print("exit scope")
}
obj2.foo()
Pickup answered 29/7, 2021 at 14:1 Comment(2)
"without increasing the reference counter" how is that different from a weak reference?Pearcy
A weak reference would be set to nil if the object is deallocated. But you would get the same Zombie message with a unowned(unsafe) reference (to an instance of a NSObject), so you are right that the Managed juggling is not needed. – So this is actually very similar to what Cristik said, only with unowned(unsafe) instead of unowned, and a subclass of NSObject, so that you really get the Zombie error message.Pickup
C
1

Swift Zombie Object

Zombie Object is a deallocated object(ref count is nil) which behaves itself like alive(receive messages). It is possible because of dangling pointer.

dangling pointer points on some address in the memory where data is unpredictable

  • Objective-C has unsafe_unretained[About] property attributes. [Example]

  • Swift has

    • unowned(unsafe)[About] reference
    • Unmanaged - a wrapper for non ARC Objc- code
unowned(unsafe) let unsafeA: A

func main() {
    let a = A() // A(ref count = 1)
    unsafeA = a 
} // A(ref count = 0), deinit() is called

func additional() {
    unsafeA.foo() //<- unexpected
}
Clintclintock answered 2/12, 2021 at 13:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.