Possible bug in stack map gen with exceptions

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


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: <https://mail.openjdk.org/pipermail/classfile-api-dev/attachments/20251105/0e4f1c2f/attachment.htm>


More information about the classfile-api-dev mailing list