Finalizers can cause a vulnerability in Java code when used to create objects. The exploit is a variation of the well-known technique of using a finalizer for resurrecting an object. When an object with a finalize() method becomes unreachable, it is put on a queue to be processed at some later time. This tip explains how the exploit works and shows how you can protect your code from it. All of the code examples are available for download.
The idea of finalizers is to allow a Java method to release any native resources that need to be returned to the operating system. Unfortunately, any Java code can be run in a finalizer, allowing code like Listing 1:
Listing 1. A resurrectable class
public class Zombie {
static Zombie zombie;
public void finalize() {
zombie = this;
}
}
|
When the Zombie finalizer is called, it takes the object being finalized — referenced by this — and stores it in the static zombie variable. Now the object is reachable again and cannot be garbage-collected.
A more insidious version of this code allows even a partially constructed object to be resurrected. Even if an object fails some criterion for correctness in its initializer, it can still be created by a finalizer, as in Listing 2:
Listing 2. Creating an illegal class
public class Zombie2 {
static Zombie2 zombie;
int value;
public Zombie2(int value) {
if(value < 0) {
throw new IllegalArgumentException("Negative Zombie2 value");
}
this.value = value;
}
public void finalize() {
zombie = this;
}
}
|
In Listing 2, the effect of the check on the value argument is negated by the existence of the finalize() method.
Of course, nobody is likely to write code like Listing 2. But a vulnerability can arise if the class is subclassed, as in Listing 3:
Listing 3. A vulnerable class
class Vulnerable {
Integer value = 0;
Vulnerable(int value) {
if(value <= 0) {
throw new IllegalArgumentException("Vulnerable value must be positive");
}
this.value = value;
}
@Override
public String toString() {
return(value.toString());
}
}
|
The Vulnerable class in Listing 3 is designed to prevent a nonpositive value of value from being set. This intent is subverted by the AttackVulnerable() method, as shown in Listing 4:
Listing 4. A class to subvert the
Vulnerable class
class AttackVulnerable extends Vulnerable {
static Vulnerable vulnerable;
public AttackVulnerable(int value) {
super(value);
}
public void finalize() {
vulnerable = this;
}
public static void main(String[] args) {
try {
new AttackVulnerable(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(vulnerable != null) {
System.out.println("Vulnerable object " + vulnerable + " created!");
}
}
}
|
In the main() method of the AttackVulnerable class, an attempt is made to instantiate a new AttackVulnerable object. Because the value of value is out of range, an exception is thrown that is caught in the catch block. The System.gc() and System.runFinalization() calls encourage the VM to run a garbage-collection cycle and run any finalizers. These calls are not necessary for the attack to succeed, but they serve to demonstrate the end result of the attack, which is that a Vulnerable object is created that has an invalid value.
Running the test case gives the following result:
java.lang.IllegalArgumentException: Vulnerable value must be positive Vulnerable object 0 created! |
Why is the value of Vulnerable 0 and not -1? Notice that in the Vulnerable constructor in Listing 3, the assignment to value does not happen until after the argument check. So value has its initial value, which in this case is 0.
This kind of attack can even be used to bypass explicit security checks. For example, the Insecure class in Listing 5 is designed to throw a SecurityException if it's run under a SecurityManager and the caller does not have permission to write to the current directory:
Listing 5.
Insecure class
import java.io.FilePermission;
public class Insecure {
Integer value = 0;
public Insecure(int value) {
SecurityManager sm = System.getSecurityManager();
if(sm != null) {
FilePermission fp = new FilePermission("index", "write");
sm.checkPermission(fp);
}
this.value = value;
}
@Override
public String toString() {
return(value.toString());
}
}
|
The Insecure class Listing 5 can be attacked in the same way as before, as shown in the AttackInsecure class in Listing 6:
Listing 6. Attacking the
Insecure class
public class AttackInsecure extends Insecure {
static Insecure insecure;
public AttackInsecure(int value) {
super(value);
}
public void finalize() {
insecure = this;
}
public static void main(String[] args) {
try {
new AttackInsecure(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(insecure != null) {
System.out.println("Insecure object " + insecure + " created!");
}
}
}
|
Running the code in Listing 6 under a SecurityManager gives the following output:
java -Djava.security.manager AttackInsecure java.security.AccessControlException: Access denied (java.io.FilePermission index write) Insecure object 0 created! |
Until the third edition of the Java Language Specification (JLS) was implemented in Java SE 6, the only ways to avoid the attack — using an initialized flag, prohibiting subclassing, or creating a final finalizer — were unsatisfactory solutions.
One way to avoid the attack is to use an initialized flag, which is set to true once an object has been correctly created. Every method in the class first checks to see if initialized is set and throws an exception if it is not. This kind of coding is tiresome to write, is easy to omit by accident, and does not stop an attacker from subclassing the method.
You can declare the class you're creating as final. This means that nobody can create a subclass of that class, which prevents the attack from working. However, this technique eliminates the flexibility of being able to extend the class in order to specialize it or add extra functionality.
You can create a finalizer for the class you're creating and declare it as final. This means that no subclass of that class can declare a finalizer. The disadvantage of this approach is that the existence of the finalizer means that the object is kept alive longer than it otherwise would be.
To make it easier to prevent this kind of attack without introducing an extra code or restrictions, the Java designers modified the JLS (see Resources) to state that if an exception is thrown in a constructor before java.lang.Object is constructed, the finalize() method of that method will not be executed.
But how can an exception be thrown before java.lang.Object is constructed? After all, the first line in any constructor must be a call to either this() or super(). If the constructor includes no such explicit call, a call to super() is implicitly added. So before an object is created, another object of the same class or of its superclass must be constructed. This eventually leads to the construction of java.lang.Object itself, and then the construction of all the subclasses, before any code from the method being constructed is executed.
To understand how an exception can be thrown before java.lang.Object is constructed, you need to understand the exact sequence of object construction. The JLS spells out the sequence explicitly.
When an object is created, the JVM:
- Allocates space for the object.
- Sets all the instance variables in the object to their default values. This includes the instance variables in the object's superclasses.
- Assigns the parameter variables for the object.
- Processes any explicit or implicit constructor invocation (a call to
this()orsuper()in the constructor). - Initializes variables in the class.
- Executes the rest of the constructor.
The key point is that the constructor's parameters are processed before any code inside the constructor is processed. This means that if you do your validation while processing the parameters, you can — by throwing an exception — prevent your class from being finalized.
This leads to a new version, shown in Listing 7, of Listing 3's Vulnerable class:
Listing 7.
Invulnerable class
class Invulnerable {
int value = 0;
Invulnerable(int value) {
this(checkValues(value));
this.value = value;
}
private Invulnerable(Void checkValues) {}
static Void checkValues(int value) {
if(value <= 0) {
throw new IllegalArgumentException("Invulnerable value must be positive");
}
return null;
}
@Override
public String toString() {
return(Integer.toString(value));
}
}
|
In Listing 7, the public constructor for Invulnerable calls a private constructor that calls the checkValues method to create its parameter. This method is called before the constructor makes its call to construct its superclass, which is the constructor for Object. So if an exception is thrown in checkValues, then the Invulnerable object will not be finalized.
The code in Listing 8 tries to attack Invulnerable:
Listing 8. Attempt to subvert the
Invulnerable class
class AttackInvulnerable extends Invulnerable {
static Invulnerable vulnerable;
public AttackInvulnerable(int value) {
super(value);
}
public void finalize() {
vulnerable = this;
}
public static void main(String[] args) {
try {
new AttackInvulnerable(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(vulnerable != null) {
System.out.println("Invulnerable object " + vulnerable + "
created!");
} else {
System.out.println("Attack failed");
}
}
}
with the addition of
} else {
System.out.println("Attack failed");
|
With Java 5, which was written to an older version of the JLS, an Invulnerable object is created:
java.lang.IllegalArgumentException: Invulnerable value must be positive Invulnerable object 0 created! |
Java SE 6 (starting with the general-availability release of Oracle's JVM and SR9 of IBM's JVM), follows the later specification, so the object is not created:
java.lang.IllegalArgumentException: Invulnerable value must be positive Attack failed |
Finalizers are an unfortunate feature of the Java language. Although the garbage collector can automatically reclaim any memory no longer used by Java objects, no such mechanism exists to recycle native resources such as native memory, file descriptors, or sockets. The standard libraries Java supplies that interface with these native resources usually have a close() method to allow a proper cleanup — but they also use finalizers to ensure no resource leak occurs if an object is not properly closed.
For other objects, finalizers are generally best avoided. There is no guarantee when a finalizer will be run, or even if it will be run at all. The existence of a finalizer means that an unreachable object cannot be garbage-collected until the finalizer has been run, and this object may be keeping yet more objects alive. This leads to an increase in the number of live objects and hence the in heap usage of the Java process.
The ability of a finalizer to revive an object destined for garbage collection is clearly an unintended consequence of the way that the finalization mechanism works. Newer implementations of the JVM now allow you to protect your code against the security implications of this effect.
| Description | Name | Size | Download method |
|---|---|---|---|
| Code samples for this tip | j-fv.zip.zip | 4KB | HTTP |
Information about download methods
Learn
- Java Language Specification: Consult the technical reference for the Java language.
- Secure Coding Guidelines for the Java Programming Language: Read these guidelines for further advice on good coding practice.
- Effective Java, 2d ed. (Joshua Bloch, Prentice Hall, 2008): This book includes a discussion of problems with finalizers, and other nuggets.
-
Language designer's notebook: Check out Brian Goetz's developerWorks series on language-design issues affecting the future of the Java language.
-
Java theory and practice: Explore Brian Goetz's long-running developerWorks series on Java programming concepts, techniques, and best practices.
-
developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.
Get products and technologies
-
Evaluate IBM products in the way that suits you best: Download a product trial, try a product online, use a product in a cloud environment, or spend a few hours in the SOA Sandbox learning how to implement Service Oriented Architecture efficiently.
Discuss
- Java security: Participate in the Java security forum on developerWorks.
- Get involved in the developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.




