Guardsquare / proguard

ProGuard, Java optimizer and obfuscator
https://www.guardsquare.com/en/products/proguard
GNU General Public License v2.0
2.87k stars 411 forks source link

VerifyError with Compose Multiplatform project #349

Open AlexeyTsvetkov opened 1 year ago

AlexeyTsvetkov commented 1 year ago

ProGuard 7.3.2 Kotlin 1.9.0 Compose Multiplatform 1.5.0-dev1114 JDK Corretto 17.0.5

Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    kotlinx/coroutines/channels/BufferOverflow.values$1bedace4()[I @3: invokevirtual
  Reason:
    Type '[I' (current frame, stack[0]) is not assignable to '[Ljava/lang/Object;'
  Current Frame:
    bci: @3
    flags: { }
    locals: { }
    stack: { '[I' }
  Bytecode:
    0000000: b200 09b6 000d c000 05b0  

Reproducer: https://github.com/AlexeyTsvetkov/compose-proguard-optimization-issue To reproduce:

  1. Run ./gradlew runDistributable. The app runs normally without ProGuard.
  2. Run ./gradlew runReleaseDistributable. The error above is thrown with ProGuard.
  3. If -dontoptimize is added to compose-desktop.pro, the app runs normally with ProGuard.

The configuration file passed to ProGuard can be found here (after runReleaseDistributable is run):

build/compose/tmp/proguardReleaseJars/root-config.pro

The ProGuard output jars can be found here (assuming ./gradlew runReleaseDistributable has run):

build/compose/binaries/main-release/app/proguard-optimizations-issue.app/Contents/app

The non-ProGuard output jars can be found here (assuming ./gradlew runDistributable has run):

build/compose/binaries/main/app/proguard-optimizations-issue.app/Contents/app
mrjameshamilton commented 1 year ago

Hi @AlexeyTsvetkov !

The issue lies in the class/unboxing/enum optimization (https://www.guardsquare.com/manual/configuration/optimizations).

Firstly, as a work around you can disable this optimization by adding -optimizations !class/unboxing/enum to your ProGuard configuration.

Unfortunately, in your sample this leads to another exception that will need further investigation.


The problem: the enum optimization replaces simple enums with integers and the problem occurs in the values() method of the enum class.

The original method looks like this:

  public static kotlinx.coroutines.channels.BufferOverflow[] values();
    descriptor: ()[Lkotlinx/coroutines/channels/BufferOverflow;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #22                 // Field $VALUES:[Lkotlinx/coroutines/channels/BufferOverflow;
         3: invokevirtual #28                 // Method "[Ljava/lang/Object;".clone:()Ljava/lang/Object;
         6: checkcast     #29                 // class "[Lkotlinx/coroutines/channels/BufferOverflow;"
         9: areturn

And the optimized method looks like this:

  public static int[] values$1bedace4();
    descriptor: ()[I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #9                  // Field $VALUES$cb20445:[I
         3: invokevirtual #13                 // Method "[Ljava/lang/Object;".clone:()Ljava/lang/Object;
         6: checkcast     #5                  // class "[I"
         9: areturn

The values array had been transformed from an enum array [Lkotlinx/coroutines/channels/BufferOverflow; to an integer array [I, which is fine.

Except the next instruction is a method call to [java/lang/Object;->clone() which is no longer correct because, as the error says, integer array is not assignable to Object array ("'[I' (current frame, stack[0]) is not assignable to '[Ljava/lang/Object;'").

This case would normally be handled in SimpleEnumDescriptorSimplifier here. In the simplifyDescriptor method which is called via visitClassConstant, the class constant is checked if it is a simple enum and if so the descriptor is replaced by an integer array descriptor:

        return isSimpleEnum(referencedClass) ?
                   descriptor.substring(0, ClassUtil.internalArrayTypeDimensionCount(descriptor)) + TypeConstants.INT :
                   descriptor;

But in the original snippet, the class referenced is [Ljava/lang/Object; rather than the enum class itself so it would not have been updated.

This seems to be a difference in the code generated by the Kotlin compiler vs Java compilers.

If you compile the following with a Java compiler (OpenJDK Runtime Environment Temurin-17.0.8+7 (build 17.0.8+7)): public enum EnumTest { A, B, C }. The values method looks like this:

  public static EnumTest[] values();
    descriptor: ()[LEnumTest;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #13                 // Field $VALUES:[LEnumTest;
         3: invokevirtual #17                 // Method "[LEnumTest;".clone:()Ljava/lang/Object;
         6: checkcast     #18                 // class "[LEnumTest;"
         9: areturn

Whereas, if you compile the following with a Kotlin compiler (kotlinc-jvm 1.8.0), enum class KotlinEnumTest { A, B, C }, the clone() method call references [Ljava/lang/Object;:

  public static KotlinEnumTest[] values();
    descriptor: ()[LKotlinEnumTest;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #22                 // Field $VALUES:[LKotlinEnumTest;
         3: invokevirtual #28                 // Method "[Ljava/lang/Object;".clone:()Ljava/lang/Object;
         6: checkcast     #29                 // class "[LKotlinEnumTest;"
         9: areturn

It looks like we'll have to take this into account to be able to correctly apply this optimization to enums generated by kotlinc.

acarlsen commented 11 months ago

Similar/same issue with possible "fix". https://github.com/JetBrains/compose-multiplatform/issues/3947