benf.org :  other :  cfr :  multi version jars

Multi Version Jars

JEP 238 describes new support in JRE 9 to allow multiple versions of the same classfile to be included in the same jar. The JRE will then use the 'latest appropriate' version. (A nice description is here.)

While this is kind of nice, it does seem like another lovely way to obfuscate behaviour!

An example please?

Consider a jar with the following content (you can get the jar from github, here)

lee@rick:/tmp$ unzip ~/code/cfr_tests/hardcoded/multijar2.jar
Archive:  /mnt/c/Users/Lee/code/cfr_tests/hardcoded/multijar2.jar
   creating: META-INF/
  inflating: META-INF/MANIFEST.MF
   creating: META-INF/versions/
   creating: META-INF/versions/11/
   creating: META-INF/versions/11/org/
   creating: META-INF/versions/11/org/benf/
   creating: META-INF/versions/11/org/benf/multijar2/
  inflating: META-INF/versions/11/org/benf/multijar2/Main$Fred.class
   creating: org/benf/multijar2/
  inflating: org/benf/multijar2/Main$1.class
  inflating: org/benf/multijar2/Main$Fred.class
  inflating: org/benf/multijar2/Main.class

Hopefully it's pretty obvious that there's an inner class ('Fred') which has a potential change in behaviour from java 10 to 11.

We can test this....

C:\code\cfr_tests\hardcoded>"c:\Program Files\Java\jdk1.6\bin"\java -jar multijar2.jar
not secret

C:\code\cfr_tests\hardcoded>"c:\Program Files\Java\jdk1.8\bin"\java -jar multijar2.jar
not secret

C:\code\cfr_tests\hardcoded>"c:\Program Files\Java\jdk-12\bin"\java -jar multijar2.jar
BIG secret

So does CFR handle this?

Prior to 0.145, CFR would process class files inside the META-INF directory as if they were normal class files, which means they would be processed, but they might collide with other class files with identical full names.

As of 0.145, CFR properly handles these, and warns you if there is alternate behaviour

If we decompile this into a temp directory using cfr-0.145+, we'll get:

lee@rick:~/code/cfr/target$ java -jar cfr-0.145.jar /tmp/multijar2.jar --outputdir /tmp/multisrc
Processing /tmp/multijar2.jar (use silent to silence)
Processing org.benf.multijar2.Main
Processing org.benf.multijar2.Main

then we can see that we've generated an extra file.

lee@rick:/tmp/multisrc$ find .
.
./META-INF
./META-INF/versions
./META-INF/versions/11
./META-INF/versions/11/org
./META-INF/versions/11/org/benf
./META-INF/versions/11/org/benf/multijar2
./META-INF/versions/11/org/benf/multijar2/Main.java
./org
./org/benf
./org/benf/multijar2
./org/benf/multijar2/Main.java
./summary.txt

Showing that we've been overridden

If we look at the source we've output, we see two versions of Main.java. The 'normal', unversioned one looks like this:

public class Main {
    public static void main(String ... args) {
        Main main = new Main();
        main.new Fred().doIt();
    }

    /*
     * Multiple versions of this class in jar - see https://www.benf.org/other/cfr/multi-version-jar.html
     */
    private class Fred {
        private Fred() {
        }

        public void doIt() {
            System.out.println("not secret");
        }
    }
}

.... and the one in META-INF/versions/11 looks like this....

public class Main {
    public static void main(String ... args) {
        Main main = new Main();
        main.new Fred().doIt();
    }

    /*
     * Multiple versions of this class in jar - see https://www.benf.org/other/cfr/multi-version-jar.html
     */
    private class Fred {
        private Fred() {
        }

        public void doIt() {
            System.out.println("BIG secret");
        }
    }
}

But hang on, that's the outer class!

This is a deliberate decision - while the jar we're reversing only overrode the inner class Fred, we don't have separate source for this. As such, CFR will regenerate the outermost class of any inner classes that have version specific class files.

You'll note that each class (including inner classes) that have version specific implementations have a comment, so it's possible to tell directly from the above that only Fred was provided.

Can I see this from the API?

Just specify that you can handle DECOMPILED_MULTIJAR - example here.


Last updated 05/2019