Possible bug in stack map gen with exceptions

David Lloyd david.lloyd at redhat.com
Wed Nov 5 14:32:08 UTC 2025


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 <david.lloyd at redhat.com> 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: <https://mail.openjdk.org/pipermail/classfile-api-dev/attachments/20251105/b322588e/attachment-0001.htm>


More information about the classfile-api-dev mailing list