Hello! I'm using Swift and Swift Package Manager to play around with the Objective-C runtime to try and understand more about how it works. I came across the object_setClass function and found it pretty neat. Unfortunately, it seems ARC gets in the way when I use it with some certain classes. Here's a minimal example:
import Foundation
import ObjectiveC
let dictClass = objc_getClass("NSString") as! AnyClass
let someObj = NSObject()
object_setClass(someObj, dictClass)
The above runs completely fine. But when we instead try to set the class to OS_object like so:
import Foundation
import ObjectiveC
let dictClass = objc_getClass("OS_object") as! AnyClass
let someObj = NSObject()
object_setClass(someObj, dictClass)
we get a trace trap. Debugging, it appears to fail in _os_object_release_without_xref_dispose with a message of "Over-release of an object". This function seems to be called as part of Swift's ARC.
Implementing this in Objective-C also fails in a similar fashion, but compiling with -fno-objc-arc seems to fix the issue. Trying to set that flag in my Package.swift for my Swift implementation results in an error on compile saying the flag is unknown.
I'm not sure if there's a different flag for Swift, but I also want to understand what exactly is going on here and why ARC is causing my code to fail in some cases, but not others. Are there some differences with the OS_* classes vs others? Is this a bug in the Objective-C runtime? And, importantly, is this something that can be fixed without disabling ARC?
Looking at the x0 register before the calls to object_setClass and swift_unknownObjectRelease, it's the same. So assuming x0 references the first argument, it appears that the object trying to be released is someObj.
OS_object is an extremely unusual ObjC class, and isn't expected to work in normal ways. It does its own custom reference counting, and expects to have a vtable adjacent to it in memory. The correct way to fix this is to stop trying to turn things that aren't OS_objects into OS_objects.
I understand that is is quite weird, but I do have a legitimate usecase for doing this. It is part of an effort of mine as a security researcher. I want to create an object that appears to be an OS_object (or some other OS_* type), but contains arbitrary data I control. If it is truly impossible to turn a non-OS_object into an OS_object in the method I described, is there another way to do it?
That's fair. libdispatch is source-available, so I can look at it's source to see if I can make sense of it. On a semi-unrelated note, is disabling ARC in Swift possible a-la -fno-objc-arc?
I mean, this is not really ARC "getting in the way". Changing the class pointer to a class with a completely different layout does not magically reinitialize the object; you're just setting a few bits in the object header and then causing every use of the object to unsafely reinterpret the existing object as a different type. Since the Objective-C object model doesn't mandate any particular format for basic things like reference counts, this is quite likely to immediately cause problems if the system does any reference-counting work on the object at all.
I do understand that, as a security researcher, these sorts of reinterpretation may be very interesting to you. This is exactly the sort of reason why Apple uses pointer authentication to try to prevent the class pointer of an object from being directly changed by an exploit, though (and hopefully it's not easy to get an exploit to call object_setClass on an arbitrary object). Finding a path where a total reinterpretation doesn't break reference counting is probably the more compelling thing to focus on in this area.
Yeah, I know changing the class pointer isn't magic. And as far as my purposes are concerned, ARC is definitely "getting in the way", but I definitely see your interpretation. And yeah, I'm definitely trying to get something that doesn't break reference counting.
Even this simple example only works because NSString is a class cluster where the base class doesn't have any ivars and doesn't override dealloc. OS_object is somewhat of a red-herring here; it happens to be unusually complicated but any obj-c class which actually does anything will break here. Turning off ARC is "fixing" the problem by making it so that you're never sending any messages to the object so it doesn't matter that the object is in an invalid state, but manually sending dealloc would break too.
TL;DR: Looking through the libdispatch code, it looks like it implements it's own API for allocating and interacting with OS_object's. I'll play around with that for now.