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