Menu

#748 Stack size becomes negative after instruction [18] invokestatic #29 in xxx

v5.3.3
closed-fixed
None
Medium
2019-05-14
2019-04-08
Mr Yang
No

I encountered a problem when using Proguard(version 5.3.3) in my android project. And I have been able to reproduce this bug stably.I simply scan the source code and I guess that it is probably a Proguard bug.
Here is the stack information:

Caused by: java.lang.IllegalArgumentException: Stack size becomes negative after instruction [18] invokestatic #29 in [xxx/pk/ProguardTest.test(Ljava/io/Closeable;Z)V]
        at proguard.classfile.attribute.visitor.StackSizeComputer.evaluateInstructionBlock(StackSizeComputer.java:349)
        at proguard.classfile.attribute.visitor.StackSizeComputer.visitExceptionInfo(StackSizeComputer.java:277)
        at proguard.classfile.attribute.CodeAttribute.exceptionsAccept(CodeAttribute.java:153)
        at proguard.classfile.attribute.visitor.StackSizeComputer.visitCodeAttribute0(StackSizeComputer.java:166)
        at proguard.classfile.attribute.visitor.StackSizeComputer.visitCodeAttribute(StackSizeComputer.java:125)
        at proguard.classfile.editor.StackSizeUpdater.visitCodeAttribute(StackSizeUpdater.java:49)
        at proguard.classfile.editor.MemberReferenceFixer.visitCodeAttribute(MemberReferenceFixer.java:322)
        at proguard.classfile.attribute.CodeAttribute.accept(CodeAttribute.java:101)
        at proguard.classfile.ProgramMethod.attributesAccept(ProgramMethod.java:81)
        at proguard.classfile.editor.MemberReferenceFixer.visitProgramMember(MemberReferenceFixer.java:285)
        at proguard.classfile.util.SimplifiedVisitor.visitProgramMethod(SimplifiedVisitor.java:92)
        at proguard.classfile.ProgramMethod.accept(ProgramMethod.java:73)
        at proguard.classfile.ProgramClass.methodsAccept(ProgramClass.java:516)
        at proguard.classfile.editor.MemberReferenceFixer.visitProgramClass(MemberReferenceFixer.java:82)
        at proguard.classfile.ProgramClass.accept(ProgramClass.java:358)
        at proguard.classfile.ClassPool.classesAccept(ClassPool.java:124)
        at proguard.optimize.Optimizer.execute(Optimizer.java:590)
        at proguard.ProGuard.optimize(ProGuard.java:328)
        at proguard.ProGuard.execute(ProGuard.java:127)
        at com.android.build.gradle.internal.transforms.BaseProguardAction.runProguard(BaseProguardAction.java:61)
        at com.android.build.gradle.internal.transforms.ProGuardTransform.doMinification(ProGuardTransform.java:253)
        ... 5 more

My testcase code:

import android.support.annotation.Keep;
import android.util.Log;

import java.io.Closeable;
import java.io.IOException;

public class ProguardTest {
    private static final int PRIORITY_MIN = 1;
    private static final String TAG = "Util";
    public static int priority = 6;

    @Keep
    public static void test(Closeable closeable, boolean hideException) throws IOException {
        if (closeable != null) {
            if (hideException) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    e(e, "IOException");
                }
            } else {
                closeable.close();
            }
        }
    }

    private static void e(Throwable t, String message) {
        if (priority > PRIORITY_MIN) {
            e(TAG, message, t);
        }
    }

    private static void e(String tag, String message, Throwable t) {
        log(priority, tag, message + "\n" + formatThrowable(t));
    }

    private static void log(int priority, String tag, String message) {
        Log.println(priority, tag, message);
    }

    private static String formatThrowable(Throwable t) {
    //An exception occurs when the Throwable variable is not used in the method. so we return a empty string
// return t.getMessage();
        return "";
    }
}

The following is the configuration information of Proguard:

-ignorewarnings
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-optimizationpasses 5     //optimizationpasses must bigger than 2 becase exception appears in the third time
-optimizations !class/unboxing/enum
#Prohibit method inlining,important
-optimizations !method/inlining/short
-optimizations !method/inlining/unique
-optimizations !method/inlining/tailrecursion

Add the above code and configuration you should also get a similar exception.So I performed a breakpoint debug on Proguard and found that there is a problem with the command corresponding to the Util.close method in the codeRemovalAdvanced method(source code Optimizer.java.LineNumber 558).

befor                                         after
aload_0 v0                                    aload_0 v0
ifnull +26                                    ifnull +27
iload_1 v1                                    iload_1 v1            
ifeq +16                                      ifeq +17                   
aload_0 v0                                    aload_0 v0              
invokeinterface #68, 256                      invokeinterface #68, 256    
return                                        return                        
ldc #70                                       ldc #70                        
invokestatic #76                              **pop2**  
return                                        invokestatic #76            
aload_0 v0                                    return                   
invokeinterface #68, 256                      aload_0 v0          
return                                        invokeinterface #68, 256  
nop                                           return                    
return                                                                
astore_1 v1                                                                
aconst_null                                                                
nop
return

There is a strange Pop2 instruction after optimization,So stack size becomes negative after invokestatic #76.

There is a process in Optimizer that removes the useless method parameters.when we remove a method parameter, the corresponding call location must pop out the corresponding operand in the stack.So I guess this is the reason.And i found a code in Proguard as below(source code:proguard.optimize.evaluation.EvaluationShrinker):

 private class MyLocalStackConsistencyFixer
    extends       SimplifiedVisitor
    implements    InstructionVisitor
    {
        public void visitAnyInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, Instruction instruction)
        {
            if (isInstructionNecessary(offset))
            {
                int popCount = instruction.stackPopCount(clazz);
                if (popCount > 0)
                {
                    TracedStack tracedStack =
                        partialEvaluator.getStackBefore(offset);

                    int stackSize = tracedStack.size();

                    int requiredPopCount  = 0;
                    int requiredPushCount = 0;
                    for (int stackIndex = stackSize - popCount; stackIndex < stackSize; stackIndex++)
                    {
                        boolean stackSimplifiedBefore =
                            isStackSimplifiedBefore(offset, stackIndex);
                        boolean stackEntryPresentBefore =
                            isStackEntryPresentBefore(offset, stackIndex);

                        if (stackSimplifiedBefore)
                        {
                            if (stackEntryPresentBefore)
                            {
                                requiredPopCount++;
                            }
                        }
                        else
                        {
                            if (!stackEntryPresentBefore)
                            {
                                requiredPushCount++;
                            }
                        }
                    }

                    // Pop some unnecessary stack entries.
                    if (requiredPopCount > 0)
                    {
                        **insertPopInstructions(offset,
                                              false,
                                              true,
                                              instruction,
                                              popCount);**
                    }

                      //other codes
                         ....
                }
              //other codes
            }
        }
   ....
   }

insertPopInstructions funcation will insert a Pop or Pop2 instruction to code attribute,according to the above example and source code , popCount will be 2 and the Throwable parameter in method e will be optimized. so requiredPopCount will be setted as 1,but the fifth parameter of the method insertPopInstructions is passed 2(popCount),so we got the strange Pop2 instruction。

This problem has been bothering me for many days.I often suspect that it is because of my code problem (I used ASM to modify the bytecode),but I turned off my function and added the above test code. I still have this problem, so I guess it is a bug in Proguard. I hope to get your help.

Discussion

  • Eric Lafortune

    Eric Lafortune - 2019-05-02

    Thanks for your detailed report and analysis! We can reproduce the problem. It's a bug in the EvaluationShrinker indeed. The exception argument in the invocation e(e, "IOException") is unused, but the code doesn't pop it properly from the stack (popping both arguments instead). We don't have a fix yet, but we're working on it.

     
  • Eric Lafortune

    Eric Lafortune - 2019-05-02
    • status: open --> open-accepted
    • assigned_to: Eric Lafortune
     
  • Eric Lafortune

    Eric Lafortune - 2019-05-07

    We've now fixed the bug for the upcoming ProGuard 6.1.0 (beta3).

     
  • Eric Lafortune

    Eric Lafortune - 2019-05-07
    • status: open-accepted --> open-fixed
     
  • Eric Lafortune

    Eric Lafortune - 2019-05-14
    • Status: open-fixed --> closed-fixed
     

Log in to post a comment.