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