[Native Image] Crema: superclass finality read from InnerClasses, rejecting valid subclasses (Kotlin enums with constant bodies)
Describe the Issue
With runtime class loading enabled, class definition rejects a class whose superclass is not final, reporting that it is:
java.lang.IncompatibleClassChangeError: Class LOuter$Barrier$1; is a subclass of final class LOuter$Barrier;
at com.oracle.svm.core.hub.registry.AbstractRuntimeClassRegistry.createClass(AbstractRuntimeClassRegistry.java:264)
at com.oracle.svm.core.hub.registry.AbstractRuntimeClassRegistry.defineClassInner(AbstractRuntimeClassRegistry.java:211)
at com.oracle.svm.core.hub.registry.AbstractRuntimeClassRegistry.defineClass(AbstractRuntimeClassRegistry.java:173)
at com.oracle.svm.core.hub.registry.ClassRegistries.defineClass(ClassRegistries.java:420)
at com.oracle.svm.core.hub.RuntimeClassLoading.defineClass(RuntimeClassLoading.java:24)
at java.base/java.lang.ClassLoader.defineClass1(ClassLoader.java:347)The superclass's own access_flags do not carry ACC_FINAL. What does carry it is the
InnerClasses attribute entry for that class. The finality test appears to consult
inner_class_access_flags rather than the class's own access_flags.
Per JVMS §4.7.6 the InnerClasses flags are the recovered source-level modifiers, provided so
reflection can report them; §5.3.5 superclass resolution tests the class's own access_flags.
HotSpot honours that split — it loads the class, and separately reports final from
Class.getModifiers().
The practical impact is large: kotlinc marks a nested enum final in InnerClasses while still
emitting subclasses for constant-specific class bodies. Every such enum therefore fails to load.
org.jetbrains.kotlin.load.java.SpecialGenericSignatures$TypeSafeBarrierDescription is one, so
loading the Kotlin compiler at run time fails.
Using the latest version of GraalVM can resolve many issues.
- Reproduced with the latest available Oracle GraalVM EA build.
GraalVM Version
java version "25.0.4.1.1" 2026-08-18 LTS
Oracle GraalVM 25.4.4.1.1-dev+1.1 (build 25.0.4.1.1+1-LTS-jvmci-25.4-b23, mixed mode, sharing)
Substrate VM Oracle GraalVM 25.4.4.1.1-dev+1.1 (build 2026-08-18 LTS, serial gc, compressed references)Operating System and Version
macOS 26 (Darwin 25.6.0), arm64.
Troubleshooting Confirmation
- I tried the suggestions in the Native Image troubleshooting guide.
Run Command
With a Crema image (-H:+RuntimeClassLoading) installed as JAVA_HOME, loading the classes below
from the class path.
Minimal reproducer — a single bit
public class Outer {
public enum Barrier {
NULL,
MAP_GET_OR_DEFAULT { // constant body -> Outer$Barrier$1 extends Outer$Barrier
@Override public String describe() { return "map-get-or-default"; }
};
public String describe() { return name(); }
}
public static void main(String[] a) {
for (Barrier b : Barrier.values()) System.out.println(b + " -> " + b.describe());
System.out.println("OK");
}
}javac Outer.java as-is runs correctly. javac emits, for Outer$Barrier:
flags: (0x4021) ACC_PUBLIC, ACC_SUPER, ACC_ENUM # class access_flags: not final
InnerClasses:
public static #60= #1 of #56; # inner_class_access_flags 0x4009: not finalNow set only ACC_FINAL in that InnerClasses entry — 0x4009 -> 0x4019, exactly what
kotlinc emits — leaving the class's own access_flags untouched at 0x4021:
| Run | Before the edit | After the edit |
|---|---|---|
| HotSpot (JDK 25) | OK |
OK |
| Crema | OK |
IncompatibleClassChangeError |
Nothing about the superclass's real modifiers changed; only the informational attribute did.
Reproducer without patching a class file
Class.forName("org.jetbrains.kotlin.load.java.SpecialGenericSignatures$TypeSafeBarrierDescription$MAP_GET_OR_DEFAULT");with kotlin-compiler-embeddable and kotlin-stdlib on the class path. HotSpot loads it; Crema
throws the error above. The disagreement is observable from Java:
- class file
access_flagsfor the superclass:0x4021— noACC_FINAL Class.getModifiers()on HotSpot:0x4019—final=true, taken fromInnerClasses
javac does not trip this because it leaves the InnerClasses entry non-final for an enum that has
constant bodies; kotlinc marks it final regardless. Both are legal.
Additional Context
Not enum-specific in principle: any class whose InnerClasses entry claims final while its own
access_flags do not will be rejected as a superclass. Enums with constant-specific bodies are
simply the common shape that produces the disagreement, and the Kotlin compiler makes it routine.
Source: oracle/graal