Menu

#647 ProGuards breaks Javaagents and tooling around it by removing argument stack map frames

v5.3.3
open-works-for-me
5
2017-06-10
2017-05-29
raphw
No

When obfuscating, ProGuard removes stack map frames for the method arguments. This breaks any usage of Java agents within programs. Using agent-based tooling like APM monitoring therefore becomes impossible when using Proguard.

For example, for a class:

class Foo {
void bar(String s) { ... }
}

Proguard could insert an empty frame into bar. Any frame should however at least always contain references for "this" and the argument "s". This information is available anways as the type signature of the method must known. Without this information, tools like Javassist or Byte Buddy cannot add code at the end of the method if the "this" instance of the arguments are required to be accessible to the code being added. To avoid this, the stack map frames can be recomputed what is straight forward for the most but very runtime intensive.

As a consequence, this currently breaks popular tools like Mockito. The current advise is to avoid using Proguard but it would be nice if the tools could work together by retaining the stack map frames that validate the signature.

Discussion

  • Eric Lafortune

    Eric Lafortune - 2017-05-29

    As far as I can see, the standard Java compilers don't produce stack map frames for method arguments either. They don't produce stack map tables at all for methods that don't need them. Am I missing something?

     
  • Eric Lafortune

    Eric Lafortune - 2017-05-29
    • status: open --> open-works-for-me
    • assigned_to: Eric Lafortune
     
  • raphw

    raphw - 2017-05-30

    I described the problem poorly. Consider that there is a branch in the method such as:

    void bar(String s) {
    if (s == null) {
    System.out.println("a");
    } else {
    System.out.println("b");
    }
    }

    In the above case, ProGuard would not retain the local variable information in the stack map frame for the branch target but for example issue an empty frame. Therefore, when adding code after this stack map frame, it is no longer legal to access "this" or "s". This breaks tooling such as Javassist or Byte Buddy which rely on the values implied by foo's signature to be available to add code at the end of the method, for example in order to measure execution time. I have experienced these problems with APM tooling and Mockito (which I help maintain) where the latter adds code in recent versions to allow mocking final methods.

     
  • Dav

    Dav - 2017-06-06

    +1 to this.

    Since upgrading Mockito in my project to a version that uses Byte Buddy, I can't get my inline-mock tests to pass. Once I disable ProGuard, all my tests pass. The exception I've been getting is:

    Caused by: java.lang.IllegalStateException: public final boolean com.package.Class.method(java.lang.String s) is inconsistent for 'this' reference: 0
    at net.bytebuddy.asm.Advice$StackMapFrameHandler$Default.translateFrame(Advice.java:1194)
    at net.bytebuddy.asm.Advice$StackMapFrameHandler$Default.translateFrame(Advice.java:1141)
    ...

    (I masked the real class/method name)

     
  • Eric Lafortune

    Eric Lafortune - 2017-06-10

    The root cause seems to be that these tools don't properly preverify the classes that they have modified. I guess they should fix that.

    One of ProGuard's goals is to compact the code, and this is one small aspect of it. I'll consider a -D option to generate the more verbose stack map tables. Even then, the optimized code may not meet the implicit assumptions these tools are making about the code.

    As a workaround, you can try leaving the method bodies unchanged:

    -dontoptimize
    -dontpreverify
    
     
    • raphw

      raphw - 2017-06-12

      The tools do verify the classes that they try to modifiy but as I explained, they can only discover that the modification was made impossible after ProGuard removed the metadata. By rendering the local variable array as empty, this would imply that the above void bar(String s) method would neither have a 'this' ot 's' variable declared as it is no longer legal to access those after the empty frame.

      javac or any other JVM-language compiler only removes temporary variables from frames after leaving a block. This is only natural when implementing a compiler as they process every instruction step-by-step where a variable is only made unavailable when exiting the block. With the method being a block of its own, all variables of the block are supposed to be available until end of a method. This works for basically any JVM language but unfortunately not for ProGuard.

      On a side note, by issuing an empty frame instead of the F_SAME frame that would normally be within the above method, ProGuard is increasing the class file's size because a full frame takes more bytes than the 'same' frame.

       

Log in to post a comment.