From rafael.wth at gmail.com Mon Nov 3 14:01:07 2025 From: rafael.wth at gmail.com (Rafael Winterhalter) Date: Mon, 3 Nov 2025 15:01:07 +0100 Subject: Extend ClassLoader API for better support of custom ModuleReader? Message-ID: Hello, I recently added support for module layers in my library and encountered a limitation. It appears that reading resources from custom layers is not fully supported. As a result, annotations on modules are not visible from my layers. My implementation uses a custom module reader that stores module data in memory and does not rely on file paths. When reading annotations from a module-info.class, the internal logic loads the class representation, locates the module?s loader (jdk.internal.loader.Loader), and calls URL findResource(String moduleName, String name) on that loader. >From my original loader, the correct URL would be available. However, Loader looks up the ModuleReference, retrieves the ModuleReader, and calls Optional find(String name). The URI is returned, but since it is not possible to attach a resource handler directly to a URI, the resolution fails when converting to a URL. Everywhere else, the Layer mechanism uses Optional open(String name), which avoids this indirection through a URL such that I can return the right byte representation for all resources. Technically, this could be resolved if ClassLoader had a method like getResourceAsStream(String moduleName, String name), allowing the Layer to invoke ModuleReader::open. This would be consistent with the other extension. Would such an extension and a change of internal use be considered viable? Thank you for your time and consideration. Best regards, Rafael -------------- next part -------------- An HTML attachment was scrubbed... URL: From david.lloyd at redhat.com Wed Nov 5 14:08:57 2025 From: david.lloyd at redhat.com (David Lloyd) Date: Wed, 5 Nov 2025 08:08:57 -0600 Subject: Possible bug in stack map gen with exceptions Message-ID: We tripped a bytecode verifier error in Semeru (IBM JDK) when using stack map generation, but I'm starting to think this could actually be a classfile API bug which HotSpot's verifier happens to miss. Basically, when there is an exception handler, and one of the control flow paths into the handler involves a local variable which is initialized using `ACONST_NULL`, then that local variable slot is defined to have a type of `null` in the exception handler, even if the variable is modified in the body of the `try` section to have some other type (like `Ljava/lang/String;` for example). Here's the meat of the bug report (the reproducer at the end is the useful bit): The following program is verifiable on Temurin but not Semeru: ``` // the method: void com.acme.FailVerify#explode(String) // max stack 1, max locals 3 0: ACONST_NULL 1: ASTORE_1 // try block starts here, catch at 8 2: LDC "Hello!" 4: ASTORE_1 // try block ends here, catch at 8 5: GOTO 9 // catch here // Stack map FULL_FRAME; locals: [java/lang/String, null] 8: ASTORE_2 // UPDATE: it fails even if this is a POP // control merges here // Stack map FULL_FRAME; locals: [java/lang/String, java/lang/String] 9: RETURN ``` The error looks like this: ``` java.lang.VerifyError: JVMVRFY021 thrown object not throwable; class=com/acme/FailVerify, method=explode(Ljava/lang/String;)V, pc=4 Exception Details: Location: com/acme/FailVerify.explode(Ljava/lang/String;)V @4: JBastore1 Reason: Type 'java/lang/String' (current frame, locals[1]) is not assignable to null (stack map, locals[1]) Current Frame: bci: @4 flags: { } locals: { 'java/lang/String', 'java/lang/String' } stack: { 'java/lang/Throwable' } Stackmap Frame: bci: @8 flags: { } locals: { 'java/lang/String', null } stack: { 'java/lang/Throwable' } Exception Handler Table: bci [2, 5] => handler: 8 Stackmap Table: full_frame(@8,{Object[#2],null},{Object[#3]}) full_frame(@9,{Object[#2],Object[#2]},{}) at java.base/java.lang.J9VMInternals.prepareClassImpl(Native Method) at java.base/java.lang.J9VMInternals.prepare(J9VMInternals.java:312) at java.base/java.lang.Class.getMethodHelper(Class.java:1336) at java.base/java.lang.Class.getMethod(Class.java:1269) (etc.) ``` A simple reproducer: ```java package whatever; import static java.lang.classfile.ClassFile.*; import static java.lang.constant.ConstantDescs.*; import java.lang.classfile.Attributes; import java.lang.classfile.ClassFile; import java.lang.classfile.Label; import java.lang.classfile.TypeKind; import java.lang.classfile.instruction.ExceptionCatch; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.lang.reflect.AccessFlag; import java.util.stream.Collectors; public class MakeFailVerify { public static void main(String[] args) throws Exception { ClassFile cf = of(StackMapsOption.GENERATE_STACK_MAPS); byte[] bytes = cf.build(ClassDesc.of("com.acme.FailVerify"), zb -> { zb.withVersion(JAVA_17_VERSION, 0); zb.withMethod("explode", MethodTypeDesc.of(CD_void, CD_String), ACC_PUBLIC | ACC_STATIC, mb -> { mb.withFlags(AccessFlag.PUBLIC, AccessFlag.STATIC); mb.withCode(cb -> { // - declare a variable init to null // - try // - assign non-null constant to variable // - goto L3 // - catch any // - store exception to new variable // - L3: return final Label L0, L1, L2, L3; L0 = cb.newLabel(); L1 = cb.newLabel(); L2 = cb.newLabel(); L3 = cb.newLabel(); // variable int storage = cb.allocateLocal(TypeKind.REFERENCE); cb.aconst_null(); cb.astore(storage); // try cb.labelBinding(L0); cb.loadConstant("Hello!"); cb.astore(storage); cb.labelBinding(L1); cb.goto_(L3); // catch/merge flow cb.labelBinding(L2); int e = cb.allocateLocal(TypeKind.REFERENCE); cb.astore(e); // exception cb.labelBinding(L3); cb.return_(); // register catch cb.with(ExceptionCatch.of(L2, L0, L1)); }); }); }); // print the method System.out.println(cf.parse(bytes).methods().get(0).toDebugString()); // print the stack map info System.out.println(cf.parse(bytes).methods().get(0).code().orElseThrow().findAttribute(Attributes.stackMapTable()).orElseThrow().entries().stream().map(Object::toString).collect(Collectors.joining("\n"))); cf.verify(bytes); ClassLoader cl = new ClassLoader() { protected Class findClass(final String name) throws ClassNotFoundException { return name.equals("com.acme.FailVerify") ? defineClass(name, bytes, 0, bytes.length) : super.findClass(name); } }; Class defined = cl.loadClass("com.acme.FailVerify"); defined.getMethod("explode", String.class).invoke(null, "Hello world"); } } ``` -- - DML ? he/him -------------- next part -------------- An HTML attachment was scrubbed... URL: From david.lloyd at redhat.com Wed Nov 5 14:32:08 2025 From: david.lloyd at redhat.com (David Lloyd) Date: Wed, 5 Nov 2025 08:32:08 -0600 Subject: Possible bug in stack map gen with exceptions In-Reply-To: References: Message-ID: I would like to add that if you insert `cb.aload(storage); cb.astore(storage);` in the reproducer after the first `cb.astore(storage);`, the stack map changes so that the variable type in the exception handler is `java/lang/String` instead of `null`. So, something is definitely weird I think. On Wed, Nov 5, 2025 at 8:08?AM David Lloyd wrote: > We tripped a bytecode verifier error in Semeru (IBM JDK) when using stack > map generation, but I'm starting to think this could actually be a > classfile API bug which HotSpot's verifier happens to miss. > > Basically, when there is an exception handler, and one of the control flow > paths into the handler involves a local variable which is initialized using > `ACONST_NULL`, then that local variable slot is defined to have a type of > `null` in the exception handler, even if the variable is modified in the > body of the `try` section to have some other type (like > `Ljava/lang/String;` for example). > > Here's the meat of the bug report (the reproducer at the end is the useful > bit): > > The following program is verifiable on Temurin but not Semeru: > > ``` > // the method: void com.acme.FailVerify#explode(String) > // max stack 1, max locals 3 > 0: ACONST_NULL > 1: ASTORE_1 > // try block starts here, catch at 8 > 2: LDC "Hello!" > 4: ASTORE_1 > // try block ends here, catch at 8 > 5: GOTO 9 > // catch here > // Stack map FULL_FRAME; locals: [java/lang/String, null] > 8: ASTORE_2 // UPDATE: it fails even if this is a POP > // control merges here > // Stack map FULL_FRAME; locals: [java/lang/String, java/lang/String] > 9: RETURN > ``` > > The error looks like this: > > ``` > java.lang.VerifyError: JVMVRFY021 thrown object not throwable; > class=com/acme/FailVerify, method=explode(Ljava/lang/String;)V, pc=4 > Exception Details: > Location: > com/acme/FailVerify.explode(Ljava/lang/String;)V @4: JBastore1 > Reason: > Type 'java/lang/String' (current frame, locals[1]) is not assignable > to null (stack map, locals[1]) > Current Frame: > bci: @4 > flags: { } > locals: { 'java/lang/String', 'java/lang/String' } > stack: { 'java/lang/Throwable' } > Stackmap Frame: > bci: @8 > flags: { } > locals: { 'java/lang/String', null } > stack: { 'java/lang/Throwable' } > Exception Handler Table: > bci [2, 5] => handler: 8 > Stackmap Table: > full_frame(@8,{Object[#2],null},{Object[#3]}) > full_frame(@9,{Object[#2],Object[#2]},{}) > > at java.base/java.lang.J9VMInternals.prepareClassImpl(Native Method) > at java.base/java.lang.J9VMInternals.prepare(J9VMInternals.java:312) > at java.base/java.lang.Class.getMethodHelper(Class.java:1336) > at java.base/java.lang.Class.getMethod(Class.java:1269) > (etc.) > ``` > > A simple reproducer: > > ```java > package whatever; > > import static java.lang.classfile.ClassFile.*; > import static java.lang.constant.ConstantDescs.*; > > import java.lang.classfile.Attributes; > import java.lang.classfile.ClassFile; > import java.lang.classfile.Label; > import java.lang.classfile.TypeKind; > import java.lang.classfile.instruction.ExceptionCatch; > import java.lang.constant.ClassDesc; > import java.lang.constant.MethodTypeDesc; > import java.lang.reflect.AccessFlag; > import java.util.stream.Collectors; > > > public class MakeFailVerify { > > public static void main(String[] args) throws Exception { > ClassFile cf = of(StackMapsOption.GENERATE_STACK_MAPS); > > byte[] bytes = cf.build(ClassDesc.of("com.acme.FailVerify"), zb -> > { > zb.withVersion(JAVA_17_VERSION, 0); > zb.withMethod("explode", MethodTypeDesc.of(CD_void, > CD_String), ACC_PUBLIC | ACC_STATIC, mb -> { > mb.withFlags(AccessFlag.PUBLIC, AccessFlag.STATIC); > mb.withCode(cb -> { > // - declare a variable init to null > // - try > // - assign non-null constant to variable > // - goto L3 > // - catch any > // - store exception to new variable > // - L3: return > final Label L0, L1, L2, L3; > L0 = cb.newLabel(); > L1 = cb.newLabel(); > L2 = cb.newLabel(); > L3 = cb.newLabel(); > // variable > int storage = cb.allocateLocal(TypeKind.REFERENCE); > cb.aconst_null(); > cb.astore(storage); > > // try > cb.labelBinding(L0); > cb.loadConstant("Hello!"); > cb.astore(storage); > cb.labelBinding(L1); > cb.goto_(L3); > // catch/merge flow > cb.labelBinding(L2); > int e = cb.allocateLocal(TypeKind.REFERENCE); > cb.astore(e); // exception > cb.labelBinding(L3); > cb.return_(); > // register catch > cb.with(ExceptionCatch.of(L2, L0, L1)); > }); > }); > }); > // print the method > > System.out.println(cf.parse(bytes).methods().get(0).toDebugString()); > > // print the stack map info > > > System.out.println(cf.parse(bytes).methods().get(0).code().orElseThrow().findAttribute(Attributes.stackMapTable()).orElseThrow().entries().stream().map(Object::toString).collect(Collectors.joining("\n"))); > cf.verify(bytes); > ClassLoader cl = new ClassLoader() { > protected Class findClass(final String name) throws > ClassNotFoundException { > return name.equals("com.acme.FailVerify") ? > defineClass(name, bytes, 0, bytes.length) : super.findClass(name); > } > }; > Class defined = cl.loadClass("com.acme.FailVerify"); > defined.getMethod("explode", String.class).invoke(null, "Hello > world"); > } > } > ``` > > > -- > - DML ? he/him > -- - DML ? he/him -------------- next part -------------- An HTML attachment was scrubbed... URL: From adam.sotona at oracle.com Wed Nov 5 15:04:19 2025 From: adam.sotona at oracle.com (Adam Sotona) Date: Wed, 5 Nov 2025 15:04:19 +0000 Subject: Possible bug in stack map gen with exceptions In-Reply-To: References: Message-ID: Hello David, According to my knowledge is this correct behavior. The stack map frame immediately following the 4: ASTORE_1 is effectively out of the try/catch block, so it does not have to be assignable to the exception handler. By adding cb.aload(storage); cb.astore(storage);? or just cb.nop()?; you make it internal part of the try/catch block and the handler frame is adjusted accordingly. Yours, Adam Sotona Confidential ? Oracle Internal From: classfile-api-dev on behalf of David Lloyd Date: Wednesday, 5 November 2025 at 15:33 To: classfile-api-dev at openjdk.org Subject: Re: Possible bug in stack map gen with exceptions I would like to add that if you insert `cb.aload(storage); cb.astore(storage);` in the reproducer after the first `cb.astore(storage);`, the stack map changes so that the variable type in the exception handler is `java/lang/String` instead of `null`. So, something is definitely weird I think. On Wed, Nov 5, 2025 at 8:08?AM David Lloyd > wrote: We tripped a bytecode verifier error in Semeru (IBM JDK) when using stack map generation, but I'm starting to think this could actually be a classfile API bug which HotSpot's verifier happens to miss. Basically, when there is an exception handler, and one of the control flow paths into the handler involves a local variable which is initialized using `ACONST_NULL`, then that local variable slot is defined to have a type of `null` in the exception handler, even if the variable is modified in the body of the `try` section to have some other type (like `Ljava/lang/String;` for example). Here's the meat of the bug report (the reproducer at the end is the useful bit): The following program is verifiable on Temurin but not Semeru: ``` // the method: void com.acme.FailVerify#explode(String) // max stack 1, max locals 3 0: ACONST_NULL 1: ASTORE_1 // try block starts here, catch at 8 2: LDC "Hello!" 4: ASTORE_1 // try block ends here, catch at 8 5: GOTO 9 // catch here // Stack map FULL_FRAME; locals: [java/lang/String, null] 8: ASTORE_2 // UPDATE: it fails even if this is a POP // control merges here // Stack map FULL_FRAME; locals: [java/lang/String, java/lang/String] 9: RETURN ``` The error looks like this: ``` java.lang.VerifyError: JVMVRFY021 thrown object not throwable; class=com/acme/FailVerify, method=explode(Ljava/lang/String;)V, pc=4 Exception Details: Location: com/acme/FailVerify.explode(Ljava/lang/String;)V @4: JBastore1 Reason: Type 'java/lang/String' (current frame, locals[1]) is not assignable to null (stack map, locals[1]) Current Frame: bci: @4 flags: { } locals: { 'java/lang/String', 'java/lang/String' } stack: { 'java/lang/Throwable' } Stackmap Frame: bci: @8 flags: { } locals: { 'java/lang/String', null } stack: { 'java/lang/Throwable' } Exception Handler Table: bci [2, 5] => handler: 8 Stackmap Table: full_frame(@8,{Object[#2],null},{Object[#3]}) full_frame(@9,{Object[#2],Object[#2]},{}) at java.base/java.lang.J9VMInternals.prepareClassImpl(Native Method) at java.base/java.lang.J9VMInternals.prepare(J9VMInternals.java:312) at java.base/java.lang.Class.getMethodHelper(Class.java:1336) at java.base/java.lang.Class.getMethod(Class.java:1269) (etc.) ``` A simple reproducer: ```java package whatever; import static java.lang.classfile.ClassFile.*; import static java.lang.constant.ConstantDescs.*; import java.lang.classfile.Attributes; import java.lang.classfile.ClassFile; import java.lang.classfile.Label; import java.lang.classfile.TypeKind; import java.lang.classfile.instruction.ExceptionCatch; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.lang.reflect.AccessFlag; import java.util.stream.Collectors; public class MakeFailVerify { public static void main(String[] args) throws Exception { ClassFile cf = of(StackMapsOption.GENERATE_STACK_MAPS); byte[] bytes = cf.build(ClassDesc.of("com.acme.FailVerify"), zb -> { zb.withVersion(JAVA_17_VERSION, 0); zb.withMethod("explode", MethodTypeDesc.of(CD_void, CD_String), ACC_PUBLIC | ACC_STATIC, mb -> { mb.withFlags(AccessFlag.PUBLIC, AccessFlag.STATIC); mb.withCode(cb -> { // - declare a variable init to null // - try // - assign non-null constant to variable // - goto L3 // - catch any // - store exception to new variable // - L3: return final Label L0, L1, L2, L3; L0 = cb.newLabel(); L1 = cb.newLabel(); L2 = cb.newLabel(); L3 = cb.newLabel(); // variable int storage = cb.allocateLocal(TypeKind.REFERENCE); cb.aconst_null(); cb.astore(storage); // try cb.labelBinding(L0); cb.loadConstant("Hello!"); cb.astore(storage); cb.labelBinding(L1); cb.goto_(L3); // catch/merge flow cb.labelBinding(L2); int e = cb.allocateLocal(TypeKind.REFERENCE); cb.astore(e); // exception cb.labelBinding(L3); cb.return_(); // register catch cb.with(ExceptionCatch.of(L2, L0, L1)); }); }); }); // print the method System.out.println(cf.parse(bytes).methods().get(0).toDebugString()); // print the stack map info System.out.println(cf.parse(bytes).methods().get(0).code().orElseThrow().findAttribute(Attributes.stackMapTable()).orElseThrow().entries().stream().map(Object::toString).collect(Collectors.joining("\n"))); cf.verify(bytes); ClassLoader cl = new ClassLoader() { protected Class findClass(final String name) throws ClassNotFoundException { return name.equals("com.acme.FailVerify") ? defineClass(name, bytes, 0, bytes.length) : super.findClass(name); } }; Class defined = cl.loadClass("com.acme.FailVerify"); defined.getMethod("explode", String.class).invoke(null, "Hello world"); } } ``` -- - DML ? he/him -- - DML ? he/him -------------- next part -------------- An HTML attachment was scrubbed... URL: