Skip to content

Latest commit

 

History

History
161 lines (109 loc) · 8.04 KB

Patching-with-Recaf.md

File metadata and controls

161 lines (109 loc) · 8.04 KB
title layout
Patching Java bytecode
default

Why

One of the challenges with Android Studio is that it isn't completely open-source. You can build most of the Android dependencies by following these steps, but there are internal libraries that are private to Google. You can recompile IntelliJ Community and the plugin, but you can also use this approach to patch existing JAR files. We used this way to verify quickly that a memory leak could be fixed!

Figuring out where to patch

You’ll want to use grep to find declarations of a class. Let’s for instance say we want to find the ProjectImportAction class that gets used in Android Studio to sync the repository:

pushd "$HOME/Applications/MDX/Electric Eel/Android Studio Preview.app/Contents/plugins/gradle/lib"
grep ProjectImportAction *.jar

You may see:

Binary file gradle-tooling-extension-api.jar matches

If you want to be sure, install the zipgrep tool (brew install zipgrep)

zipgrep ProjectImportAction gradle-tooling-extension-api.jar

This result will show:

org/jetbrains/plugins/gradle/model/ProjectImportAction$1.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$2.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$3.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$4.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$5.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$6.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$7$1.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$7.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$8$1.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$8$2.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$8.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$AllModels.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$DefaultBuild$DefaultProjectModel.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$DefaultBuild.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$GradleBuildConsumer.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$ModelConverter.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$MyBuildController.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$NoopConverter.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportActionWithCustomSerializer$1.c

You can also install a GUI viewer to view the JAR file too:

brew install jadx

At the terminal, run jadx-gui gradle-tooling-extension-api.jar to confirm the classes are located there.

Patching

Download Recaf. Make sure to download the JAR file that is labeled -with-dependencies.jar.

image

Then run Recaf:

java -jar ~/Downloads/recaf-2.21.13-J8-jar-with-dependencies.jar

Before loading any file, we are going to need to make a few tweaks in the Config.

Click on the Decompile option and choose the Procyon disassembler:

image

Next, uncheck the Generate missing classes option. You will want to add libraries manually, especially for projects that depend on many other JAR files. With this option turns on, Recaf seems unable to resolve these missing classes.

image

Exit the Config menu by clicking on the Red icon:

image

Use the FileLoad to load the JAR file (e.g. gradle-tooling-extension-api.jar).

Recaf depends on having all the classes necessary to recompile the JAR file. If you edit a class and hit Cmd-S to save it, you will likely see errors at the bottom. If there are Cannot find symbol errors and red lines on the import statements for the class file, it means that you may will need to use the Add library feature:

image

To find any class references, you will likely need to use a combination of grep and using zipgrep/ jadx-gui on individual files to confirm these are the right ones.

Once you add a library, you will see the top-left corner turns into a drop-down. Only the JAR labeled Primary can be modified. For instance, here is an example of the dependent libraries for the gradle-tooling-extension-api.jar that were needed to update the ProjectImportAction class. There could be other JAR files depending on which class file you edit.

image

You can use the Export workspace option to save this configuration. The contents will be saved as JSON and can be reloaded again by Recaf.

Once you’ve identified the changes you want to make, you may need to deal with casting issues by the decompiler:

image

Most of the issues can be solved by deleting the casting issues in question. The decompiler is not perfect, so using Recaf’s assembler mostly involves removing these errors. For instance, this line can be changed from:

addFetchedModelActions.addAll((Collection<? extends List<Runnable>>)controller.run((Collection<? extends BuildAction<?>>)buildActions));

To:

addFetchedModelActions.addAll(controller.run(buildActions));

The one exception is the ‘cannot find symbol class AllModels class, which needs to be referenced as ProjectImportAction.AllModels because it’s actually compiled in a different file.

Adding other Java classes

Recaf doesn’t have a way to add Java classes, but you can do it pretty easily by creating the Java class and compiling it. We can simply create SimpleThreadFactory.java in a local editor. Note that we set the package to org.jetbrains.plugins.gradle.model so we will need to take care of this case later.

package org.jetbrains.plugins.gradle.model;

import java.util.concurrent.ThreadFactory;

public final class SimpleThreadFactory implements ThreadFactory {
    public Thread newThread(Runnable r) {
       return new Thread(r, "idea-tooling-model-converter");
   }
 }

Then you can create the .class file.

javac SimpleThreadFactory.java

The next step is to add it to the JAR we want to patch. Let’s make a backup just in case:

cp gradle-tooling-extension-api.jar gradle-tooling-extension-api.jar.orig

You can then add this SimpleThreadFactory.class by relocating it in the right directory:

mkdir -p org/jetbrains/plugins/gradle/model
mv SimpleThreadFactory.class org/jetbrains/plugins/gradle/model
zip gradle-tooling-extension-api.jar org/jetbrains/plugins/gradle/model/SimpleThreadFactory.class

Then go ahead and do your patching work with this new added class!