Remembering non-boot-classloader LinkageErrors as resolution errors

Gilles DUBOSCQ gilles.m.duboscq at oracle.com
Tue Jan 28 09:10:20 UTC 2025


Hi,

While looking into how exceptions are recorded during resolution failures (e.g., indy, condy, or class constants) I noticed a few potential issues if the exception is a LinkageError subclass that doesn't come from the boot class path.
Since resolution errors are recorded symbolically, when trying to re-throw an existing failure, the exception type is looked up in the boot class loader. For types that can't be found there, it results in a NoClassDefFoundError being thrown instead.
Even if the type was properly remembered, the way exceptions are re-constructed might fail for custom exception types since it assume that either a) there is no message and the exception type as a no-args constructor, or b) there is a message and the exception type as a constructor with a String argument.
It's easy to build LinkageError subclasses that don't respect this.

See a test that exercises this below.

Does this make sense? What should happen here? jvms §5.4.3 says that it should "fail with the same error that was thrown as a result of the initial resolution attempt".
How to interpret "same" here? Currently it seems to be interpreted as same type and message.
It seems hard to robustly re-construct exceptions from user-defined types. One option would be rethrow the same instance for non-boot-cp LinkageErrors, but the stack trace will be the original one.

Also note that the hotspot behaviour of only recording LinkageErrors makes sense but is not exactly as specified. That was noted in JDK-6308271 which recommended to update the spec. It's marked as resolved but the spec was apparently never changed.

 Gilles


* get asm: `curl -O https://repo1.maven.org/maven2/org/ow2/asm/asm/9.7.1/asm-9.7.1.jar`
* build `javac -g -cp asm-9.7.1.jar Test.java`
* run `java -ea -cp asm-9.7.1.jar:. Test`
Test.java
```
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.IADD;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.PUTSTATIC;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_8;

import java.io.Serial;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

public class Test {
    private static final TestLoader LOADER = new TestLoader(ClassLoader.getSystemClassLoader());
    public static final String DUMMY = "bc.indy.Dummy";

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Class<?> dummyClass = LOADER.loadClass(DUMMY);
        // a boostram method that throw a IllegalArgumentException causes a BootstrapMethodError
        try {
            dummyClass.getDeclaredMethod("test0").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof BootstrapMethodError : e.getCause();
            assert e.getCause().getCause() instanceof IllegalArgumentException : e.getCause().getCause();
            assert "First Failure".equals(e.getCause().getCause().getMessage()) : e.getCause().getCause().getMessage();
        }
        // using the same call site results in the same error
        try {
            dummyClass.getDeclaredMethod("test0").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof BootstrapMethodError : e.getCause();
            // the cause for indy is not remembered
            if (e.getCause().getCause() != null) {
                assert e.getCause().getCause() instanceof IllegalArgumentException : e.getCause().getCause();
                assert "First Failure".equals(e.getCause().getCause().getMessage()) : e.getCause().getCause().getMessage();
            }
        }
        // using a new call site results in a new link attempt
        try {
            dummyClass.getDeclaredMethod("test1").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert Error.class == e.getCause().getClass() : e.getCause().getClass();
            assert "Second Failure".equals(e.getCause().getMessage()) : e.getCause().getMessage();
        }
        // Errors that are not LinkageErrors are not sticky
        try {
            dummyClass.getDeclaredMethod("test1").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert Error.class == e.getCause().getClass() : e.getCause().getClass();
            assert "Third Failure".equals(e.getCause().getMessage()) : e.getCause().getMessage();
        }
        // throwing a custom LinkageError with a no-arg constructor only
        try {
            dummyClass.getDeclaredMethod("test2").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError0 : e.getCause();
            assert e.getCause().getMessage() == null : e.getCause().getMessage();
            assert e.getCause().getCause() == null : e.getCause().getCause();
        }
        // again
        try {
            dummyClass.getDeclaredMethod("test2").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError0 : e.getCause();
            assert e.getCause().getMessage() == null : e.getCause().getMessage();
            assert e.getCause().getCause() == null : e.getCause().getCause();
        }
        // throwing a custom LinkageError with a no-arg constructor only and a cause
        try {
            dummyClass.getDeclaredMethod("test3").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError1 : e.getCause();
            assert e.getCause().getMessage() == null : e.getCause().getMessage();
            assert RuntimeException.class == e.getCause().getCause().getClass() : e.getCause().getCause().getClass();
            assert "Custom1 Cause".equals(e.getCause().getCause().getMessage()) : e.getCause().getCause().getMessage();
        }
        // again
        try {
            dummyClass.getDeclaredMethod("test3").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError1 : e.getCause();
            assert e.getCause().getMessage() == null : e.getCause().getMessage();
            // the cause for indy is not remembered
            if (e.getCause().getCause() != null) {
                assert RuntimeException.class == e.getCause().getCause().getClass() : e.getCause().getCause().getClass();
                assert "Custom1 Cause".equals(e.getCause().getCause().getMessage()) : e.getCause().getCause().getMessage();
            }
        }
        // throwing a custom LinkageError with a message constructor that drops the message
        try {
            dummyClass.getDeclaredMethod("test4").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError2 : e.getCause().getClass();
            assert e.getCause().getMessage() == null : e.getCause().getMessage();
            assert e.getCause().getCause() == null : e.getCause().getCause();
        }
        // again
        try {
            dummyClass.getDeclaredMethod("test4").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError2 : e.getCause().getClass();
            assert e.getCause().getMessage() == null : e.getCause().getMessage();
            assert e.getCause().getCause() == null : e.getCause().getCause();
        }
        // throwing a custom LinkageError with a no-args constructor that adds a message
        try {
            dummyClass.getDeclaredMethod("test5").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError3 : e.getCause().getClass();
            assert "Foo".equals(e.getCause().getMessage()) : e.getCause().getMessage();
            assert e.getCause().getCause() == null : e.getCause().getCause();
        }
        // again
        try {
            dummyClass.getDeclaredMethod("test5").invoke(null);
            assert false : "Should have thrown an exception";
        } catch (InvocationTargetException e) {
            assert e.getCause() instanceof CustomLinkageError3 : e.getCause().getClass();
            assert "Foo".equals(e.getCause().getMessage()) : e.getCause().getMessage();
            assert e.getCause().getCause() == null : e.getCause().getCause();
        }
        // working no-op
        dummyClass.getDeclaredMethod("test6").invoke(null);
    }

    private static final class TestLoader extends ClassLoader {

        TestLoader(ClassLoader parent) {
            super(parent);
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            if (name.equals(DUMMY)) {
                byte[] bytes = generateClass();
                return defineClass(name, bytes, 0, bytes.length);
            }
            return super.findClass(name);
        }

        public static byte[] generateClass() {
            ClassWriter cw = new ClassWriter(COMPUTE_FRAMES);
            String internalDummy = DUMMY.replace('.', '/');
            cw.visit(V1_8, ACC_PUBLIC | ACC_SUPER, internalDummy, null, "java/lang/Object", null);

            FieldVisitor fv = cw.visitField(ACC_PRIVATE | ACC_STATIC, "count", "I", null, null);
            fv.visitEnd();

            String bootstrapDesc = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/Object;";
            Handle bootstrap = new Handle(Opcodes.H_INVOKESTATIC, internalDummy, "bootstrap", bootstrapDesc, false);

            MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test0", "()V", null, null);
            mv.visitInvokeDynamicInsn("dyn", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test1", "()V", null, null);
            mv.visitInvokeDynamicInsn("dyn", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test2", "()V", null, null);
            mv.visitInvokeDynamicInsn("dyn", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test3", "()V", null, null);
            mv.visitInvokeDynamicInsn("dyn", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test4", "()V", null, null);
            mv.visitInvokeDynamicInsn("dyn", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test5", "()V", null, null);
            mv.visitInvokeDynamicInsn("dyn", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test6", "()V", null, null);
            mv.visitInvokeDynamicInsn("dyn", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PRIVATE | ACC_STATIC, "bootstrap", bootstrapDesc, null, null);
            mv.visitFieldInsn(GETSTATIC, internalDummy, "count", "I");
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_1);
            mv.visitInsn(IADD);
            mv.visitFieldInsn(PUTSTATIC, internalDummy, "count", "I");
            mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Test.class), "bootstrapHelper", "(I)Ljava/lang/Object;", false);
            mv.visitInsn(ARETURN);
            mv.visitMaxs(1, 3);
            mv.visitEnd();

            cw.visitEnd();

            return cw.toByteArray();
        }
    }

    @SuppressWarnings("unused")
    public static Object bootstrapHelper(int count) {
        switch (count) {
            case 0:
                throw new IllegalArgumentException("First Failure");
            case 1:
                throw new Error("Second Failure");
            case 2:
                throw new Error("Third Failure");
            case 3:
                throw new CustomLinkageError0();
            case 4:
                CustomLinkageError1 error = new CustomLinkageError1();
                error.initCause(new RuntimeException("Custom1 Cause"));
                throw error;
            case 5:
                throw new CustomLinkageError2("Custom2 Message");
            case 6:
                throw new CustomLinkageError3();
        }
        return new ConstantCallSite(MethodHandles.dropReturn(MethodHandles.constant(int.class, 0)));
    }

    public static final class CustomLinkageError0 extends LinkageError {
        @Serial private static final long serialVersionUID = 2531603139987257998L;
    }

    public static final class CustomLinkageError1 extends LinkageError {
        @Serial private static final long serialVersionUID = 3451603130517257323L;
    }

    public static final class CustomLinkageError2 extends LinkageError {
        @Serial private static final long serialVersionUID = -5099886956530290307L;

        public CustomLinkageError2(String message) {
            super(null);
        }
    }

    public static final class CustomLinkageError3 extends LinkageError {
        @Serial private static final long serialVersionUID = 4226129985618631909L;

        public CustomLinkageError3() {
            super("Foo");
        }
    }
}
```


More information about the hotspot-runtime-dev mailing list