Resurrecting a PhantomReferenceJan 20, 2015
In this post, you'll learn about Java necromancy: how to resurrect objects back from the dead. We will abuse a recently fixed security bug in the JDK to trick the garbage collector to get ahold of an object to which no living object should have a connection.
In essence, the garbage collector collects all unreachable objects; objects to which there are no references anymore. But there are multiple levels of reachability, types of references and phases in an object's dying process. Before we start resurrecting phantoms, let's briefly explore some background and the history of resurrection in Java.
WeakReference is a pointer to an object that does not prevent it from being garbage collected. When the garbage collector determines that an object is weakly reachable (only reachable through
WeakReferences), it also clears those references, severing their last link to the world of the living. To notify the owner of the reference it then adds the
WeakReference to its
ReferenceQueue; its queue of death records. For example
WeakHashMap, which uses
WeakReferences internally, polls its queue to expunge stale entries from its hash table.
Surprisingly, even after a
WeakReference has been enqueued, the object it pointed to may still be revived: in the same dying phase as the enqueuing, the object is marked for finalization. That means its
finalize() method will be called. That usually temporarily but possibly permanently revives the object, because the body of the finalize method has access to
this. This is a well known documented flaw, and one of the reasons you should avoid finalization whenever you can.
In short, the purpose of finalization is to signal that an object is dying, but ironically that same signal resurrects it from its weak death.
An object is another step closer to death when it becomes phantom reachable. That means it's not strongly, softly or weakly reachable, any finalization has already happened, but there's still a
PhantomReference to it. Just like for
WeakReferences, if the garbage collector determines that an object is phantom reachable, it adds it to the
ReferenceQueue. For example direct ByteBuffers rely on the queuing of phantom references to free native memory. This is supposed to mean that the object will stay dead: it should never become reachable again by Java code.
Side note: One difference with an enqueued weak reference is that it is not cleared: the
PhantomReference still has a reference to the object after enqueuing, but there's no honest way to get it out:
PhantomReference.get() always returns null. You could use reflection to access the
Reference.referent field, but if we go that way we can throw all guarantees out the window. It's cheating, like changing the matrix, and not allowed by the JVM if a security manager is enabled.
- So, where can an honest necromancer get ahold of a dead object around here?
Remember, we could resurrect a dying weakly reachable object only because we got help from our omniscient friend the garbage collector. He's not constrained by reachability when calling finalize() to signal a presumed impending death. We can't use that same trick here, but there's a similar signal from the garbage collector that we can abuse: the
to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected (from its javadoc).
Loose translation: it's magically given references of our choice from our helpful friend after stuff becomes hard to reach.
- Thank you mr Garbage Collector, you just performed resurrection by appending the appropriately reachable reference.
- GC: No, I only append the phantom, not the actual referred dead object.
And here comes the twist...
- Ow, I don't care about the referred object, that's just a decoy. It's the phantom itself we're resurrecting.
- Nice try, but I'm not that easily tricked: I'd garbage collect any unreachable phantom before I'd consider adding it to its queue. Dead phantoms stay dead.
- Well, it's not quite dead yet, it's still dying. It's not unreachable; it's phantom reachable. A phantom reachable phantom!
*GC mind blown*
So this allows you to trick the garbage collector into resurrection a
PhantomReference. The phantom is allowed to bring its friends: it can have ordinary, strong, references to any other object that will then be resurrected along with it.
Taking a step back, our goal was to get ahold of an object after some victim PhantomReference declared it dead. We do that by resurrecting another associated PhantomReference that in turn also is only phantom reachable. That's a complicated dance of three phantoms; the diagram below makes this choreography a bit easier to follow.
Resurrection in practice
Let's look at a concrete resurrection in action of a
DirectByteBuffer directly accesses native memory which is freed when its
Cleaner (a special kind of
PhantomReference) would be enqueued. That gives us a use-after-free vulnerability.
When the garbage collector realizes that the objects in gray here are only phantom reachable he calls the Cleaner and enqueues them in their queue; adding the reference in green. That makes the
ResurrectedReference and thereby the
DirectByteBuffer strongly reachable again. If we then access the buffer, we're accessing freed memory...
Here's the code for that:
Timeline: Alexey Makhmutov sent this as a puzzle a la "try to break this PhantomReference" in June 2014. I reported it to Oracle after solving it and realizing it was a security vulnerability. Oracle, released a Critical Patch Update for this today (2015-01-20).
Thanks Alexey, and sorry for ruining your puzzle with this report.
Exploitability: Here's a proof-of-concept crash. Given the nature of the bug, it can likely be exploited to execute arbitrary code.
The fix: This vulnerability is now fixed by treating cleaners as an even weaker type of reference than phantom references. See this commit (more pointers welcome!). I haven't had the time to actually test it yet, but this implies that normal PhantomReferences are still resurrectable; but that just doesn't have a security impact anymore.
Finalization is resurrection because it hands you back the dead object to finalize it.
PhantomReferences and their queue prevent that by giving you a different object instead, an indirect reference. But they forget to check if that reference also doesn't happen to be in the middle of its own death scene.
What does this mean for Java users and developer? Nothing at all. If you're not running hostile Java code on your machine and relying on the Java sandbox for protection, you have nothing to worry about. If you do, e.g. if you're running Java applets or running a multi-tenant application server, then you should install the latest Java update; as you should always be doing already anyways. There probably are a bunch of worse vulnerabilities in the same update, as usual.