#14468·graal

[Native Image] Crema: superclass finality read from InnerClasses, rejecting valid subclasses (Kotlin enums with constant bodies)

Author: sgammonCreated Sep 17, 2026Updated Sep 17, 2026

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

java
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 final

Now 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

java
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_flags for the superclass: 0x4021 — no ACC_FINAL
  • Class.getModifiers() on HotSpot: 0x4019final=true, taken from InnerClasses

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.