Guardsquare / proguard

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

Library interface with generics fails #5

Open zdary opened 5 years ago

zdary commented 5 years ago

Hi,

proguard does not recognize interface with gererics in a library. It's implementation method names are then changed.

This interface is in library.jar

public interface MyInterface<T> {
    void setTypedSomething(T var1);

    T getTypedSomething();

    void normalMethod();
}

Implementation class is in in.jar

public class MyImplFail implements MyInterface<MyPojo> {
    MyPojo myPojo;

    public MyImplFail() {
    }

    public void setTypedSomething(MyPojo myPojo) {
        this.myPojo = myPojo;
    }

    public MyPojo getTypedSomething() {
        return this.myPojo;
    }

    public void normalMethod() {
        this.myPojo = null;
    }
}

Proguard result

public final class a implements MyInterface<c> {
    private c a;

    public a() {
    }

    private void a(c var1) {
        this.a = var1;
    }

    private c a() {
        return this.a;
    }

    public final void normalMethod() {
        this.a = null;
    }
}

The problem is the generics. If your implementation uses as a parameter then Proguard works.

public final class b implements MyInterface<Object> {
    private c a;

    public b() {
    }

    public final void setTypedSomething(Object var1) {
        this.a = (c)var1;
    }

    public final Object getTypedSomething() {
        return this.a;
    }

    public final void normalMethod() {
        this.a = null;
    }
}

I have attached a sample project demonstrating this bug here https://sourceforge.net/p/proguard/bugs/765/

Thank you, Martin

netomi commented 4 years ago
netomi commented 4 years ago

Thanks for the report, I attached the reproducible sample from the sourceforge ticket here. It is clear what is going wrong, ProGuard does not obfuscate the respective interface methods in case of generics correctly as it looks only at the descriptor of methods atm.

This does not look like a quick and easy fix, but we will try to fix it asap.

netomi commented 4 years ago

I checked the sample and inspected the resulting class files. The type erased methods from the interface are still there and are not obfuscated:

  public final java.lang.Object getTypedSomething();
  public final void setTypedSomething(java.lang.Object);

Also when I test it with the same setup as you with a Main method, it works correctly. Can you possibly update the sample to illustrate the exact problem you are facing?

zdary commented 4 years ago

@netomi the method which need to stay is:

public MyPojo getTypedSomething();

not public Object getTypedSomething();

Object is fine as that really hides the generics. All other types fail

netomi commented 4 years ago

So when modifying the Main class to include something like that:

MyPojo arg = inter.getTypedSomething();

the resulting bytecode looks like that:

        22: invokeinterface #7,  1            // InterfaceMethod MyInterface.getTypedSomething:()Ljava/lang/Object;
        27: checkcast     #4                  // class MyPojo

so the compiler will invoke the type erased method and add a checkcast instruction. So the specialized method is not needed in general.

Can you describe in detail in which scenario the specialized method would be called and what is your tooling in this scenario (kotlin, certain java version)?

zdary commented 4 years ago

Hi @netomi

ok, let me take you through the process. We have a class in a library. - that's the jar which will not be obfuscated and needs to stay as-is.

public interface MyInterface<T> {
T getTypedSomething();

Then we have an implementation in the code which will be obfustated.

public class MyImplFail implements MyInterface<MyPojo> {
MyPojo getTypedSomething() {...}

Notice that T changes to MyPojo.

Expected :

public class MyImplFail implements MyInterface<MyPojo> {
MyPojo getTypedSomething();

Actual:

public final class a implements MyInterface<c> {
   private c a() ;

So the actual problem is that proguard renames MyPojo getTypedSomething() to c a() which breaks the interface which is located in a library and is not obfuscated.

public interface MyInterface<T> {
    T getTypedSomething();

I hope I explain myself clearly :) Feel free to reach out if not.

netomi commented 4 years ago

The problem description is clear, I can also see that result in the sample project:

MyImplFail -> a: 14:14:MyPojo getTypedSomething() -> a 1:1:java.lang.Object getTypedSomething() -> getTypedSomething

so the specialized method MyPojo getTypedSomething() gets obfuscated as ProGuard does not recognize it being an implementation method of the MyInterface interface.

Now, what we dont understand is in which cases that actually results in something wrong or an error. We did various tests and they all worked as the type erased method will be called at runtime.

Can you give more details about the environment where this leads to a runtime / compile error?

zdary commented 4 years ago

oh, we develop a plugin in java for enterprise level software. Since our code is just a plugin the "library" code loads the obfuscated implementation classes and uses interface methods to invoke the code

netomi commented 4 years ago

What are the target / source levels for the java plugin?

zdary commented 4 years ago

1.8 / 1.8

netomi commented 4 years ago

ok thanks, can you maybe share the snippet of code that loads / uses the obfuscated plugin which results in the crash I guess?

Also what source / target level is the consumer of the library using? Is the consumer maybe developed in another JVM language, like kotlin?

That would help us to create a reproducible testcase.

zdary commented 4 years ago

Hi, intellij uses a mix of kotlin and java.

The plugin developers declare all the extension points in plugin.xml each extension point has its own xml tag. For example

<projectService serviceImplementation="com.intellij.idea.plugin.hybris.flexibleSearch.mixins.FlexibleSearchElementFactory"/>
<applicationService serviceImplementation="com.intellij.idea.plugin.hybris.notification.NotificationManager"/>

Looking at the source code. They use Class.forName to get the class Class.forName(myClassName, true, pluginClassLoader)); and then use reflection to check that the class implements the right interface and then get the constructor.

try {
      Constructor<T> constructor = aClass.getDeclaredConstructor();
      try {
        constructor.setAccessible(true);
      }
      catch (SecurityException e) {
        return aClass.newInstance();
      }
      return constructor.newInstance();
    }

For the test case, the main method should be in a library and should use the above to load the class then access it using the interface.

netomi commented 4 years ago

I continued reproducing the issue, this time I tried to instantiate the class in kotlin and access the typed method, the resulting byte code is like that:

        47: invokeinterface #39,  1           // InterfaceMethod test/MyInterface.getTypedSomething:()Ljava/lang/Object;
        52: checkcast     #41                 // class java/lang/String

So the type erased method is being called and then a cast is done.

The corresponding kotlin code is like that:

        val o:MyInterface<String> = Class.forName("MyImplFail")?.newInstance() as MyInterface<String>
        val f = o.getTypedSomething()

Can you share a stacktrace that you get when loading the obfuscated plugin fails?