JBoss.org Community Documentation

3.2.2.3. LinkageErrors - Making Sure You Are Who You Say You Are

Loading constraints validate type expectations in the context of class loader scopes to ensure that a class X is consistently the same class when multiple class loaders are involved. This is important because Java allows for user defined class loaders. Linkage errors are essentially an extension of the class cast exception that is enforced by the VM when classes are loaded and used.

To understand what loading constraints are and how they ensure type-safety we will first introduce the nomenclature of the Liang and Bracha paper along with an example from this paper. There are two type of class loaders, initiating and defining. An initiating class loader is one that a ClassLoader.loadClass method has been invoked on to initiate the loading of the named class. A defining class loader is the loader that calls one of the ClassLoader.defineClass methods to convert the class byte code into a Class instance. The most complete expression of a class is given by <C,Ld>Li , where C is the fully qualified class name, Ld is the defining class loader, and Li is the initiating class loader. In a context where the initiating class loader is not important the type may be represented by <C,Ld>, while when the defining class loader is not important, the type may be represented by CLi . In the latter case, there is still a defining class loader, it's just not important what the identity of the defining class loader is. Also, a type is completely defined by <C,Ld>. The only time the initiating loader is relevant is when a loading constraint is being validated. Now consider the classes shown in Example 3.5, “Classes demonstrating the need for loading constraints”.

class <C,L1> {
    void f() {
        <Spoofed, L1>L1x = <Delegated, L2>L2
        x.secret_value = 1; // Should not be allowed
    }
}
class <Delegated,L2> {
    static <Spoofed, L2>L3 g() {...}
    }
}
class <Spoofed, L1> {
    public int secret_value;
}
class <Spoofed, L2> {
    private int secret_value;
}

Example 3.5. Classes demonstrating the need for loading constraints


The class C is defined by L1 and so L1 is used to initiate loading of the classes Spoofed and Delegated referenced in the C.f() method. The Spoofed class is defined by L1, but Delegated is defined by L2 because L1 delegates to L2. Since Delegated is defined by L2, L2 will be used to initiate loading of Spoofed in the context of the Delegated.g() method. In this example both L1 and L2 define different versions of Spoofed as indicated by the two versions shown at the end of Example 3.5, “Classes demonstrating the need for loading constraints”. Since C.f() believes x is an instance of <Spoofed,L1> it is able to access the private field secret_value of <Spoofed,L2> returned by Delegated.g() due to the 1.1 and earlier Java VM's failure to take into account that a class type is determined by both the fully qualified name of the class and the defining class loader.

Java addresses this problem by generating loader constraints to validate type consistency when the types being used are coming from different defining class loaders. For the Example 3.5, “Classes demonstrating the need for loading constraints” example, the VM generates a constraint SpoofedL1=SpoofedL2 when the first line of method C.f() is verified to indicate that the type Spoofed must be the same regardless of whether the load of Spoofed is initiated by L1 or L2. It does not matter if L1 or L2, or even some other class loader defines Spoofed. All that matters is that there is only one Spoofed class defined regardless of whether L1 or L2 was used to initiate the loading. If L1 or L2 have already defined separate versions of Spoofed when this check is made a LinkageError will be generated immediately. Otherwise, the constraint will be recorded and when Delegated.g() is executed, any attempt to load a duplicate version of Spoofed will result in a LinkageError.

Now let's take a look at how a LinkageError can occur with a concrete example. Example 3.6, “A concrete example of a LinkageError” gives the example main class along with the custom class loader used.

package org.jboss.book.jmx.ex0;
import java.io.File;
import java.net.URL;

import org.apache.log4j.Logger;
import org.jboss.util.ChapterExRepository;
import org.jboss.util.Debug;

/** 
 * An example of a LinkageError due to classes being defined by more
 * than one class loader in a non-standard class loading environment.
 *
 * @author Scott.Stark@jboss.orgn
 * @version $Revision: 1.9 $
 */
public class ExLE
{
    public static void main(String[] args) 
	throws Exception
    {
        ChapterExRepository.init(ExLE.class);

        String chapDir = System.getProperty("j2eechapter.dir");
        Logger ucl0Log = Logger.getLogger("UCL0");
        File jar0 = new File(chapDir+"/j0.jar");
        ucl0Log.info("jar0 path: "+jar0.toString());
        URL[] cp0 = {jar0.toURL()};
        Ex0URLClassLoader ucl0 = new Ex0URLClassLoader(cp0);
        Thread.currentThread().setContextClassLoader(ucl0);
        
                     Class ctxClass1 = ucl0.loadClass("org.jboss.book.jmx.ex0.ExCtx");
                  
        
                  
                     Class obj2Class1 = ucl0.loadClass("org.jboss.book.jmx.ex0.ExObj2");
                  
        StringBuffer buffer = new StringBuffer("ExCtx Info");
        Debug.displayClassInfo(ctxClass1, buffer, false);
        ucl0Log.info(buffer.toString());
        buffer.setLength(0);
        buffer.append("ExObj2 Info, UCL0");
        Debug.displayClassInfo(obj2Class1, buffer, false);
        ucl0Log.info(buffer.toString());
        
        File jar1 = new File(chapDir+"/j1.jar");
        Logger ucl1Log = Logger.getLogger("UCL1");
        ucl1Log.info("jar1 path: "+jar1.toString());
        URL[] cp1 = {jar1.toURL()};
        Ex0URLClassLoader ucl1 = new Ex0URLClassLoader(cp1);
        
                     Class obj2Class2 = ucl1.loadClass("org.jboss.book.jmx.ex0.ExObj2");
                  
        buffer.setLength(0);
        buffer.append("ExObj2 Info, UCL1");
        Debug.displayClassInfo(obj2Class2, buffer, false);
        ucl1Log.info(buffer.toString());
        
        
                     ucl0.setDelegate(ucl1);
                  
        try {
            ucl0Log.info("Try ExCtx.newInstance()");
            
                     Object ctx0 = ctxClass1.newInstance();
                  
            ucl0Log.info("ExCtx.ctor succeeded, ctx0: "+ctx0);
        } catch(Throwable e) {
            ucl0Log.error("ExCtx.ctor failed", e);
        }
    }
}
package org.jboss.book.jmx.ex0;

import java.net.URLClassLoader;
import java.net.URL;

import org.apache.log4j.Logger;

/** 
 * A custom class loader that overrides the standard parent delegation
 * model
 *
 * @author Scott.Stark@jboss.org
 * @version $Revision: 1.9 $
 */
public class Ex0URLClassLoader extends URLClassLoader
{
    private static Logger log = Logger.getLogger(Ex0URLClassLoader.class);
    private Ex0URLClassLoader delegate;

    public Ex0URLClassLoader(URL[] urls)
    {
        super(urls);
    }
    
    void setDelegate(Ex0URLClassLoader delegate)
    {
        this.delegate = delegate;
    }
    
    protected synchronized Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        Class clazz = null;
        if (delegate != null) {
            log.debug(Integer.toHexString(hashCode()) +
		      "; Asking delegate to loadClass: " + name);
            clazz = delegate.loadClass(name, resolve);
            log.debug(Integer.toHexString(hashCode()) +
		      "; Delegate returned: "+clazz);
        } else {
            log.debug(Integer.toHexString(hashCode()) + 
		      "; Asking super to loadClass: "+name);
            clazz = super.loadClass(name, resolve);
            log.debug(Integer.toHexString(hashCode()) + 
		      "; Super returned: "+clazz);
        }
        return clazz;
    }

    protected Class findClass(String name)
        throws ClassNotFoundException
    {
        Class clazz = null;
        log.debug(Integer.toHexString(hashCode()) + 
		  "; Asking super to findClass: "+name);
        clazz = super.findClass(name);
        log.debug(Integer.toHexString(hashCode()) + 
		  "; Super returned: "+clazz);
        return clazz;
    }
}

Example 3.6. A concrete example of a LinkageError


The key component in this example is the URLClassLoader subclass Ex0URLClassLoader. This class loader implementation overrides the default parent delegation model to allow the ucl0 and ucl1 instances to both load the ExObj2 class and then setup a delegation relationship from ucl0 to ucl1. At lines 30 and 31. the ucl0 Ex0URLClassLoader is used to load the ExCtx and ExObj2 classes. At line 45 of ExLE.main the ucl1 Ex0URLClassLoader is used to load the ExObj2 class again. At this point both the ucl0 and ucl1 class loaders have defined the ExObj2 class. A delegation relationship from ucl0 to ucl1 is then setup at line 51 via the ucl0.setDelegate(ucl1) method call. Finally, at line 54 of ExLE.main an instance of ExCtx is created using the class loaded via ucl0. The ExCtx class is the same as presented in Example 3.4, “The ExIAEd class used to demonstrate IllegalAccessException due to duplicate class loaders”, and the constructor was:

public ExCtx() 
    throws IOException
{
    value = new ExObj();
    Logger log = Logger.getLogger(ExCtx.class);
    StringBuffer buffer = new StringBuffer("ctor.ExObj");
    Debug.displayClassInfo(value.getClass(), buffer, false);
    log.info(buffer.toString());
    ExObj2 obj2 = value.ivar;
    buffer.setLength(0);
    buffer = new StringBuffer("ctor.ExObj.ivar");
    Debug.displayClassInfo(obj2.getClass(), buffer, false);
    log.info(buffer.toString());
}    

Now, since the ExCtx class was defined by the ucl0 class loader, and at the time the ExCtx constructor is executed, ucl0 delegates to ucl1, line 24 of the ExCtx constructor involves the following expression which has been rewritten in terms of the complete type expressions:

<ExObj2,ucl0>ucl0 obj2 = <ExObj,ucl1>ucl0 value * ivar

This generates a loading constraint of ExObj2ucl0 = ExObj2ucl1 since the ExObj2 type must be consistent across the ucl0 and ucl1 class loader instances. Because we have loaded ExObj2 using both ucl0 and ucl1 prior to setting up the delegation relationship, the constraint will be violated and should generate a LinkageError when run. Run the example using the following command:

[examples]$ ant -Dchap=jmx -Dex=0e run-example
Buildfile: build.xml
...
[java] java.lang.LinkageError: loader constraints violated when linking 
           org/jboss/book/jmx/ex0/ExObj2 class
[java]     at org.jboss.book.jmx.ex0.ExCtx.<init>(ExCtx.java:24)
[java]     at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
[java]     at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessor
           Impl.java:39)     
[java]     at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructor
           AccessorImpl.java:27)
[java]     at java.lang.reflect.Constructor.newInstance(Constructor.java:494)
[java]     at java.lang.Class.newInstance0(Class.java:350)
[java]     at java.lang.Class.newInstance(Class.java:303)
[java]     at org.jboss.book.jmx.ex0.ExLE.main(ExLE.java:53)                        

As expected, a LinkageError is thrown while validating the loader constraints required by line 24 of the ExCtx constructor.