Modifying NMS from a plugin

Last week, I wanted to create a proof of concept of a plugin that could modify NMS (net.minecraft.server) code that could be easily distributed through any public plugin pages such as SpigotMC or BukkitDev. I tried using ASM, but it ended up being too complicated to use so I dove into the fantastic world of the internet and I found a pretty cool library called Javassist.

What's Javassist?

Javassist, as their project page defines it, is "a class library for editing bytecodes in Java". Bytecode is the compiled code that is generated when you run a compiler such as javac, Maven or Gradle. This bytecode is later read by the Java virtual machine, which converts it into machine code that is run by the CPU. I'm gonna use Maven to add it to my project:

<dependency>
  <groupId>javassist</groupId>
  <artifactId>javassist</artifactId>
  <version>3.12.1.GA</version>
</dependency>

The biggest problem I bumped into with ASM was the way CraftBukkit loads plugin classes. First of all, the NMS server classes are loaded; later, the worlds are stored in memory and finally, every plugin under the plugins/ directory is loaded by a JavaPluginLoader. What this means is that by the time our plugin is loaded, most of the NMS classes have already been loaded (and some of them will also have running instances).

In order to fix this problem, you will need to load your plugin before world loading occurs to minimize the loaded NMS classes by the JVM. Simply add this line of code to your plugin.yml file:

load: STARTUP

This will ensure your plugin classes are loaded right after CraftBukkit starts. Now, let's head into the bytecode modifying part, shall we?

Javassist provides two levels of API: source level and bytecode level. The first one allows us to write a String containing Java code that will be compiled on the fly for you, while the second one only gives you the possibility to modify Java bytecode, which can be intimidating to someone starting to learn to code. Here's an example:

public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class #2
   #2 = Utf8 HelloWorld
   #3 = Class #4
   #4 = Utf8 java/lang/Object
   #5 = Class #6 
   #6 = Utf8 java/lang/System
   #7 = Class #8 
   #8 = Utf8 java/io/PrintStream
   #9 = String #10 
  #10 = Utf8 Hello World
  #11 = Fieldref #5.#12 
  #12 = NameAndType #13:#14 
  #13 = Utf8 out
  #14 = Utf8 Ljava/io/PrintStream;
  #15 = Methodref #7.#16 
  #16 = NameAndType #17:#18
  #17 = Utf8 println
  #18 = Utf8 (Ljava/lang/String;)V
  #19 = Utf8 main
  #20 = Utf8 ([Ljava/lang/String;)V
  #21 = Utf8 Code
{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #11 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #9  // String Hello World
         5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

That was the Java's (formatted) bytecode of a Hello world program. Don't you agree it's a bit messy with all the references and keywords?

That's why I went the easy way and used the source level library. In order to modify a class we need to create a ClassPool and convert a Java class into a Javassist CtClass which we can then freely modify.

ClassPool pool = ClassPool.getDefault();
CtClass clazz;

try {
  clazz = pool.get("net.minecraft.server.v1_8_r3.ChunkProviderServer");
} catch (NotFoundException e) {
  e.printStackTrace();
}

If you want to support multiple NMS versions at the same time, you can get the version code the server is running by splitting the net.minecraft.server.[version].Server package name by using some string splitting:

String version = null;

try {
    version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3];
} catch (ArrayIndexOutOfBoundsException e) {
    getLogger().severe("Running custom NMS version, couldn't patch NMS classes");
    // TODO Disable the plugin
}

Next, we need to get the method we want to modify. Javassist provides a getter by method signature, but as NMS implementations may change the required params/returns, we can convert the method array into a Stream and filter them by name to get the only one we need.

String methodName = "saveChunk";

CtMethod saveMethod = Arrays.stream(clazz.getMethods())
                          .filter(method -> method.getName().equals(methodName))
                          .findAny()
                          .orElse(null);

The fun part comes now that we have the CtMethod and the ultimate reason why I chose Javassist: the #setBody(String) method. Here are some examples of it in action:

saveMethod.setBody("{}");

saveMethod.setBody("{ System.out.println(\"Trying to save chunk!\"); }");

Finally, we need to apply the changes we made by calling clazz.toClass() which will perform the compiling and bytecode replacing. Note that compilation times are usually really fast, but you should use manual bytecode manipulation whenever possible.

By the way, this bytecode manipulation is only performed when the server starts, so it will far faster than using reflection for modifying fields. Your players won't even notice you're modifying any classes because there's no performance loss.

The main drawback of this implementation is that the Javassist library weights 634 kb (which is pretty small considering it contains a Java compiler) that you will need to shade with Maven because Bukkit doesn't include it by default. Add this to your project's size and you can end up with a really fat JAR.

There are probably more pros/cons I can add to this list, but this is the bare minimum for comparison. Do your own research and try using reflection/ASM/Javassist before commiting one way or the other.

© Hugmanrique. Made with