Skip to content

Commit

Permalink
Version 1.4 (#61)
Browse files Browse the repository at this point in the history
* core(cfois): support multi-dimensional arrays (#19)

* Improve compatibility with older versions and newer Fabric servers (#43)

* Abstracted logger to fall back to native if Log4J isn't present

* chore: shadow gson into the final jar to support older versions correctly

* fix: initialize config check request with TLS 1.2
Java 7 seems to have TLS 1.2 disabled by default

---------

Co-authored-by: ThePixelbrain <[email protected]>

* fix/feat(modlauncher): load deserialized classes from the correct class loader (#45)

* feat: also redirect SerializationUtils.deserialize in patched classes (#46)

* feat: change NativeLogger to println
Although println is definitely not the most
fancy logging implementation as a fallback,
there were problems with the default Java
logger in < MC 1.7. This is the most stable for
these versions and also provides advantages
for Fabric servers as well

* chore: exclude java 9 files from the bundle jar

* fix: bundle ASM lib to fix patching issues in some Fabric setups

* feat: allow configuration of remote config loading

* fix: create config directory if it does not exist

* fix: add workaround to classloading issues in Forge < 1.13 when the agent is used

* feat: add timeout to remote config reading
60 seconds should be sufficient (I hope)

* feat: use gson with pretty printing for the default local config

* fix(modlauncher): return additionalClassesLocator even when the agent is loaded
This should fix some classloading issues in the
Java 8 version of Modlauncher when the agent is used

---------

Co-authored-by: ThatGamerBlue <[email protected]>
Co-authored-by: ThePixelbrain <[email protected]>
  • Loading branch information
3 people authored Aug 6, 2023
1 parent f21d2a4 commit 1969191
Show file tree
Hide file tree
Showing 16 changed files with 374 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

import io.dogboy.serializationisbad.core.Patches;
import io.dogboy.serializationisbad.core.SerializationIsBad;
import org.objectweb.asm.tree.ClassNode;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class SIBTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String classNameDots = className.replace('/', '.');
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
if (className == null) return classfileBuffer;
if ("net/minecraft/launchwrapper/ITweaker".equals(className)) SerializationIsBadAgent.insertLaunchWrapperExclusion();

if (Patches.getPatchModuleForClass(classNameDots) == null) return classfileBuffer;
String classNameDots = className.replace('/', '.');

SerializationIsBad.logger.info("Applying patches to " + classNameDots);
if (Patches.getPatchModuleForClass(classNameDots) == null) return classfileBuffer;

ClassNode classNode = Patches.readClassNode(classfileBuffer);
Patches.applyPatches(classNameDots, classNode);
return Patches.writeClassNode(classNode);
return Patches.patchClass(classfileBuffer, classNameDots, false);
} catch (Throwable e) {
SerializationIsBad.logger.error("Failed to run agent class transformer", e);
return classfileBuffer;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import java.io.File;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class SerializationIsBadAgent {

Expand All @@ -12,4 +14,25 @@ public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new SIBTransformer());
}

/**
* When SIB is loaded as a JVM agent in a LaunchWrapper environment,
* we need to add an exclusion to the LaunchClassLoader so SIB classes
* always get loaded with the parent classloader.
* Otherwise, mod classes create a separate SIB instance which is not initialized.
*
* I know this is an ugly hack, please don't judge me.
*/
static void insertLaunchWrapperExclusion() {
try {
Class<?> launchClass = Class.forName("net.minecraft.launchwrapper.Launch");
Field classLoaderField = launchClass.getDeclaredField("classLoader");
Object classLoader = classLoaderField.get(null);
Class<?> classLoaderClass = Class.forName("net.minecraft.launchwrapper.LaunchClassLoader");
Method addClassLoaderExclusion = classLoaderClass.getDeclaredMethod("addClassLoaderExclusion", String.class);
addClassLoaderExclusion.invoke(classLoader, "io.dogboy.serializationisbad");
} catch (Exception e) {
SerializationIsBad.logger.error("Failed to add LaunchWrapper exclusion", e);
}
}

}
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id 'java'
id "com.github.johnrengelman.shadow" version "8.1.1" apply false
}

subprojects {
Expand All @@ -19,12 +20,14 @@ subprojects {

task bundleJar(type: Jar, dependsOn: subprojects.assemble) {
archivesBaseName = rootProject.name
from project(':core').configurations.archives.allArtifacts.files.collect { zipTree(it) }
from zipTree(new File (project(':core').buildDir, "libs/core-${project(':core').version}-all.jar")) // i'm currently unable to correctly get the shadowJar output, so this should work for now
from project(':legacyforge').configurations.archives.allArtifacts.files.collect { zipTree(it) }
from project(':modlauncher').configurations.archives.allArtifacts.files.collect { zipTree(it) }
from project(':agent').configurations.archives.allArtifacts.files.collect { zipTree(it) }
from 'LICENSE.txt'

exclude 'META-INF/versions/**'

manifest {
from subprojects.collect{ it.jar.manifest }
}
Expand Down
18 changes: 17 additions & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
apply plugin: 'java-library'
apply plugin: 'com.github.johnrengelman.shadow'

dependencies {
implementation 'com.google.code.gson:gson:2.10.1'
api 'org.apache.logging.log4j:log4j-api:2.20.0'
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
api 'org.ow2.asm:asm-tree:9.5'
}

Expand All @@ -19,3 +20,18 @@ jar {
])
}
}

shadowJar {
dependencies {
exclude(dependency('org.apache.logging.log4j:log4j-api'))
}

relocate 'com.google.gson', 'io.dogboy.serializationisbad.shadow.gson'
relocate 'org.objectweb.asm', 'io.dogboy.serializationisbad.shadow.asm'
}

artifacts {
archives shadowJar
}

build.dependsOn shadowJar
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

public class ClassFilteringObjectInputStream extends ObjectInputStream {
private final PatchModule patchModule;
private final ClassLoader parentClassLoader;

public ClassFilteringObjectInputStream(InputStream in, PatchModule patchModule) throws IOException {
public ClassFilteringObjectInputStream(InputStream in, PatchModule patchModule, ClassLoader parentClassLoader) throws IOException {
super(in);
this.patchModule = patchModule;
this.parentClassLoader = parentClassLoader;
}

public ClassFilteringObjectInputStream(InputStream in, PatchModule patchModule) throws IOException {
this(in, patchModule, null);
}

private boolean isClassAllowed(String className) {
if (className.startsWith("[L") && className.endsWith(";")) {
className = className.substring(2, className.length() - 1);
} else if (className.startsWith("L") && className.endsWith(";")) {
// strip all array dimensions, just get the base type
while (className.startsWith("[")) {
className = className.substring(1);
}

if (className.startsWith("L") && className.endsWith(";")) {
className = className.substring(1, className.length() - 1);
}

Expand Down Expand Up @@ -51,7 +61,34 @@ protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, Clas
throw new ClassNotFoundException("Class " + desc.getName() + " is not allowed to be deserialized");
}

return super.resolveClass(desc);
if (this.parentClassLoader == null) {
return super.resolveClass(desc);
}

String name = desc.getName();
try {
return Class.forName(name, false, this.parentClassLoader);
} catch (ClassNotFoundException ex) {
Class<?> cl = ClassFilteringObjectInputStream.primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

private static final HashMap<String, Class<?>> primClasses = new HashMap<>(8, 1.0F);
static {
ClassFilteringObjectInputStream.primClasses.put("boolean", boolean.class);
ClassFilteringObjectInputStream.primClasses.put("byte", byte.class);
ClassFilteringObjectInputStream.primClasses.put("char", char.class);
ClassFilteringObjectInputStream.primClasses.put("short", short.class);
ClassFilteringObjectInputStream.primClasses.put("int", int.class);
ClassFilteringObjectInputStream.primClasses.put("long", long.class);
ClassFilteringObjectInputStream.primClasses.put("float", float.class);
ClassFilteringObjectInputStream.primClasses.put("double", double.class);
ClassFilteringObjectInputStream.primClasses.put("void", void.class);
}

}
64 changes: 61 additions & 3 deletions core/src/main/java/io/dogboy/serializationisbad/core/Patches.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

public class Patches {

Expand All @@ -24,20 +28,22 @@ public static PatchModule getPatchModuleForClass(String className) {
return null;
}

public static ClassNode readClassNode(byte[] classBytecode) {
private static ClassNode readClassNode(byte[] classBytecode) {
ClassNode classNode = new ClassNode();
ClassReader classReader = new ClassReader(classBytecode);
classReader.accept(classNode, 0);
return classNode;
}

public static byte[] writeClassNode(ClassNode classNode) {
private static byte[] writeClassNode(ClassNode classNode) {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);
return writer.toByteArray();
}

public static void applyPatches(String className, ClassNode classNode) {
private static void applyPatches(String className, ClassNode classNode, boolean passClassLoader) {
SerializationIsBad.logger.info("Applying patches to " + className);

for (MethodNode methodNode : classNode.methods) {
InsnList instructions = methodNode.instructions;
for (int i = 0; i < instructions.size(); i++) {
Expand All @@ -53,17 +59,69 @@ public static void applyPatches(String className, ClassNode classNode) {
((MethodInsnNode) instruction).owner = "io/dogboy/serializationisbad/core/ClassFilteringObjectInputStream";
((MethodInsnNode) instruction).desc = "(Ljava/io/InputStream;Lio/dogboy/serializationisbad/core/config/PatchModule;)V";

if (passClassLoader) {
((MethodInsnNode) instruction).desc = "(Ljava/io/InputStream;Lio/dogboy/serializationisbad/core/config/PatchModule;Ljava/lang/ClassLoader;)V";
}

InsnList additionalInstructions = new InsnList();
additionalInstructions.add(new LdcInsnNode(className));
additionalInstructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "io/dogboy/serializationisbad/core/Patches",
"getPatchModuleForClass", "(Ljava/lang/String;)Lio/dogboy/serializationisbad/core/config/PatchModule;", false));

if (passClassLoader) {
additionalInstructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
additionalInstructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false));
additionalInstructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getClassLoader", "()Ljava/lang/ClassLoader;", false));
}

instructions.insertBefore(instruction, additionalInstructions);

SerializationIsBad.logger.info(" (2/2) Redirecting ObjectInputStream to ClassFilteringObjectInputStream in method " + methodNode.name);
} else if (instruction.getOpcode() == Opcodes.INVOKESTATIC
&& instruction instanceof MethodInsnNode
&& "org/apache/commons/lang3/SerializationUtils".equals(((MethodInsnNode) instruction).owner)
&& "deserialize".equals(((MethodInsnNode) instruction).name)) {

((MethodInsnNode) instruction).owner = "io/dogboy/serializationisbad/core/Patches";

if ("(Ljava/io/InputStream;)Ljava/lang/Object;".equals(((MethodInsnNode) instruction).desc)) {
((MethodInsnNode) instruction).desc = "(Ljava/io/InputStream;Lio/dogboy/serializationisbad/core/config/PatchModule;)Ljava/lang/Object;";
} else if ("([B)Ljava/lang/Object;".equals(((MethodInsnNode) instruction).desc)) {
((MethodInsnNode) instruction).desc = "([BLio/dogboy/serializationisbad/core/config/PatchModule;)Ljava/lang/Object;";
} else {
throw new RuntimeException("Unknown desc for SerializationUtils.deserialize: " + ((MethodInsnNode) instruction).desc);
}

InsnList additionalInstructions = new InsnList();
additionalInstructions.add(new LdcInsnNode(className));
additionalInstructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "io/dogboy/serializationisbad/core/Patches",
"getPatchModuleForClass", "(Ljava/lang/String;)Lio/dogboy/serializationisbad/core/config/PatchModule;", false));

instructions.insertBefore(instruction, additionalInstructions);

SerializationIsBad.logger.info(" Redirecting SerializationUtils.deserialize to Patches in method " + methodNode.name);
}
}
}
}

public static byte[] patchClass(byte[] classBytes, String className, boolean passClassLoader) {
ClassNode classNode = Patches.readClassNode(classBytes);
Patches.applyPatches(className, classNode, passClassLoader);
return Patches.writeClassNode(classNode);
}

@SuppressWarnings("unchecked")
public static <T> T deserialize(InputStream inputStream, PatchModule patchModule) {
try (ClassFilteringObjectInputStream objectInputStream = new ClassFilteringObjectInputStream(inputStream, patchModule)) {
return (T) objectInputStream.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static <T> T deserialize(byte[] objectData, PatchModule patchModule) {
return Patches.deserialize(new ByteArrayInputStream(objectData), patchModule);
}

}
Loading

1 comment on commit 1969191

@Sjinetf
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the patch, I didn't even know about this till now. 👌🏼

Please sign in to comment.