Remembering non-boot-classloader LinkageErrors as resolution errors

Gilles DUBOSCQ gilles.m.duboscq at oracle.com
Thu Jan 30 10:05:51 UTC 2025


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
* 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
```

 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