Resurrecting a PhantomReference

Jan 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.

Weakness

A 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.

Phantoms

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 PhantomReference's 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.

Resurrection theory

- 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 ReferenceQueue,
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. 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:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

/**
 * Acts like a reference that's weaker than a phantom reference, but
 * allows you to revive the referent after it becomes phantom reachable.
 */
public class Necromancer<T> {
  /** The queue which gets the ResurrectedReference enqueued */
  private final ReferenceQueue<Object> q = new ReferenceQueue<>();
  /** Making ResurrectedReference phantom reachable (instead of unreachable) */
  private final PhantomReference<ResurrectedReference<T>> pr;

  public Necromancer(T o) {
    ResurrectedReference<T> toBeResurrected = new ResurrectedReference<>(o, q);
    pr = new PhantomReference<>(toBeResurrected, new ReferenceQueue<>());
  }

  public T waitForDeathAndResurrect() {
    ResurrectedReference<T> ref;
    do {
      System.gc();
      ref = (ResurrectedReference<T>) q.poll();
    } while (ref == null);
    return ref.o;
  }

  static class ResurrectedReference<T> extends PhantomReference<Object> {
    /** Strong reference to object to resurrect along with this */
    private final T o;

    public ResurrectedReference(T o, ReferenceQueue<Object> q) {
      super(new Object() /* decoy */, q);
      this.o = o;
    }
  }
}
ByteBuffer getResurrectedByteBuffer(int size) {
  Necromancer<ByteBuffer> necromancer =
      new Necromancer<>(ByteBuffer.allocateDirect(size));
  return necromancer.waitForDeathAndResurrect();
}

Vulnerability info

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.

Conclusion

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.