Remembering non-boot-classloader LinkageErrors as resolution errors

David Holmes david.holmes at oracle.com
Fri Jan 31 03:06:27 UTC 2025


Hi Gilles,

On 30/01/2025 8:05 pm, Gilles DUBOSCQ wrote:
> Hi David,
> 
> Thanks for your reply.
> Indeed, it very much looks like the original intention was to only consider LinkageError subclasses defined in java.base.
> However LinkageError can be freely subclassed. Combined with the fact that bootstrap methods are user-defined, it means they can throw user-defined LinkageError subclasses.
> 
> What's happening is that if the user-defined bootstrap method throws an instance of a user-defined LinkageError subclass,
> * `java.lang.invoke.CallSite#makeSite` lets that through directly (it doesn't wrap `java.lang.Error` instances)
> * since it's an instance of LinkageError, it gets saved as a resolution error in the constant pool & resolution error table

Yikes! Okay that is not the intention as far as I am aware. Only 
LinkageErrors thrown in the context of things that throw "linkage 
errors" should be saved as a resolution error. Anything from a BSM is 
supposed to be handled according to different rules. But to be honest 
I'm not that familiar with the detailed workings and rules of BSMs.

> * it's then rethrown at the invokedynamic that triggered this resolution
> * the next time this invokedynamic is executed, the VM tries to throw the saved resolution error
> * it tries to locate the exception type in the boot class loader but fails. leading to a NoClassDefFoundError
> 
> Now the question is: in the second execution, was the "same error" thrown?
> I would have expected both executions to at least throw execeptions with the same type.
> 
> I think your suggestion of wrapping non-boot-class-loader LinkageError subclasses could work:
> `java.lang.invoke.CallSite#makeSite` could wrap any non-boot-class-loader LinkageError in a BootstrapMethodError:
> ```
> diff --git a/src/java.base/share/classes/java/lang/invoke/CallSite.java b/src/java.base/share/classes/java/lang/invoke/CallSite.java
> index e9e3477f96f..d100278fa31 100644
> --- a/src/java.base/share/classes/java/lang/invoke/CallSite.java
> +++ b/src/java.base/share/classes/java/lang/invoke/CallSite.java
> @@ -333,6 +333,9 @@ static CallSite makeSite(MethodHandle bootstrapMethod,
>               // method is inaccessible, or say OutOfMemoryError
>               // See the "Linking Exceptions" section for the invokedynamic
>               // instruction in JVMS 6.5.
> +           if (e.getClass().getClassLoader() != null) {
> +                throw new BootstrapMethodError("CallSite bootstrap method initialization exception", e);
> +            }
>               throw e;
>           } catch (Throwable ex) {
>               // Wrap anything else in BootstrapMethodError
> ```

Yes that seems a reasonable approach. The exact details may vary 
depending on how things work out as we trawl through the spec.

Can you file a JBS issue with the above details?

Thanks,
David
-----

>   Gilles
> 
> 
> ________________________________________
> From: David Holmes <david.holmes at oracle.com>
> Sent: Thursday, 30 January 2025 03:12
> To: Gilles DUBOSCQ <gilles.m.duboscq at oracle.com>; hotspot-runtime-dev at openjdk.org <hotspot-runtime-dev at openjdk.org>
> Subject: Re: Remembering non-boot-classloader LinkageErrors as resolution errors
>   
> Hi Gilles,
> 
> On 28/01/2025 7:10 pm, Gilles DUBOSCQ wrote:
>> 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.
> 
> The (original?) expectation is that all such LinkageErrors are defined
> by the JVMS and as such can only involve those defined in the java.base
> module. I can't quite parse the example you give below but if a BSM can
> introduce a user-defined LinkageError subclass as a LinkageError thrown
> by the VM, then that seems a hole in the JVMS. Either we have to
> accommodate such errors within 5.4.3 or we have to specify them to be
> wrapped by regular LinkageErrors. Current 5.4.3 states:
> 
> "If an error occurs during resolution of the symbolic reference, then it
> is either (i) an instance of IncompatibleClassChangeError (or a
> subclass); (ii) an instance of Error (or a subclass) that arose from
> resolution or invocation of a bootstrap method; or (iii) an instance of
> LinkageError (or a subclass) that arose because class loading failed or
> a loader constraint was violated."
> 
> So BSM errors should be handled as per (ii). So I'm not completely clear
> on what is happening with your test versus what you think should be
> happening.
> 
> Trying to accommodate 5.4.3 in all circumstances, in terms of preserving
> the original exception is problematic.
> 
> David
> -----
> 
>> 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