Possible bug in stack map gen with exceptions
Adam Sotona
adam.sotona at oracle.com
Wed Nov 5 15:04:19 UTC 2025
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 <classfile-api-dev-retn at openjdk.org> on behalf of David Lloyd <david.lloyd at redhat.com>
Date: Wednesday, 5 November 2025 at 15:33
To: classfile-api-dev at openjdk.org <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 <david.lloyd at redhat.com<mailto: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/0eff5eab/attachment-0001.htm>
More information about the classfile-api-dev
mailing list