Skip to content

Commit

Permalink
Use custom class loader to replace Unsafe in transforming BootstrapLa…
Browse files Browse the repository at this point in the history
…uncher
  • Loading branch information
InitAuther97 committed Jan 21, 2025
1 parent 95832d6 commit 9352928
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,107 @@
package io.izzel.arclight.boot.application;

import cpw.mods.cl.ModuleClassLoader;
import io.izzel.arclight.api.Unsafe;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;

import java.io.IOException;
import java.io.InputStream;
import java.security.ProtectionDomain;
import java.util.Map;
import java.util.function.BiFunction;

public class BootstrapTransformer {
private static final Map<String, BiFunction<ClassLoader, ProtectionDomain, Exception>> SUPPORTED = Map.of(
"cpw.mods.bootstraplauncher.BootstrapLauncher",
BootstrapTransformer::transformBootstrapLauncher
);

public static Class<?> loadTransform(String className, ClassLoader cl, ProtectionDomain domain) throws Exception {
if (!SUPPORTED.containsKey(className)) {
throw new UnsupportedOperationException("Transformation for "+className+" is not supported");

/*
* The implementation is affected by BootstrapLauncher and ModLauncher
* Be sure to check for updates.
*/
public class BootstrapTransformer extends ClassLoader {

private static final String cpwClass = "cpw.mods.bootstraplauncher.BootstrapLauncher";

private final ProtectionDomain domain = getClass().getProtectionDomain();

@SuppressWarnings({"unused", "unchecked"})
public static void onInvoke$BootstrapLauncher(String[] args, ModuleClassLoader moduleCl) {
try {
Class<ApplicationBootstrap> arclightBootClz = (Class<ApplicationBootstrap>) moduleCl.loadClass("io.izzel.arclight.boot.application.ApplicationBootstrap");
Object instance = arclightBootClz.getConstructor().newInstance();
arclightBootClz.getMethod("accept", String[].class).invoke(instance, (Object) args);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}

public BootstrapTransformer(ClassLoader appClassLoader) {
super("arclight_bootstrap", appClassLoader);
}

/*
* The class to transform can be resolved by AppClassLoader.
* We have to break the delegation model to intercept class loading.
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}

// The class is not loaded. Are we going to intercept?
// The inner classes and the outer class should be loaded
// in the same ClassLoader to avoid inter-module access issues.
if (!name.contains(cpwClass)) {
// Delegate to parent.
// parent.loadClass is inaccessible from here.
// This ClassLoader will only load the launcher
// and then a new ClassLoader, whose parent is
// platform ClassLoader (null), will load the game.
return super.loadClass(name, resolve);
}

Class<?> clz;
try {
clz = loadTransform(name);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("Unexpected exception loading " + name);
}

if (resolve) {
resolveClass(clz);
}
return clz;
}
}

/*
* findClass() is invoked when parent (in this case AppClassLoader)
* cannot find the corresponding class. In this case we can't find either.
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

public Class<?> loadTransform(String className) throws IOException {
if (className.equals(cpwClass)) {
var file = cpwClass.replace('.', '/').concat(".class");
try (var inputStream = getResourceAsStream(file)) {
if (inputStream == null) {
throw new RuntimeException("getResourceAsStream can't read BootstrapLauncher.class");
}
var transformed = transformBootstrapLauncher(inputStream);
return defineClass(cpwClass, transformed, 0, transformed.length, domain);
}
} else if (className.contains(cpwClass)) {
var file = className.replace('.', '/').concat(".class");
try (var inputStream = getResourceAsStream(file)) {
if (inputStream == null) {
throw new RuntimeException("getResourceAsStream can't read "+file.substring(file.lastIndexOf('/')));
}
var bytes = inputStream.readAllBytes();
return defineClass(className, bytes, 0, bytes.length, domain);
}
}
var ex = SUPPORTED.get(className).apply(cl, domain);
if (ex != null) throw ex;
return cl.loadClass(className);
throw new UnsupportedOperationException("Transformation for " + className + " is not supported");
}

/*
Expand All @@ -31,81 +110,64 @@ public static Class<?> loadTransform(String className, ClassLoader cl, Protectio
* Modify BootstrapLauncher to use ApplicationBootstrap directly so a change in module name won't
* affect launch process.
*/
public static Exception transformBootstrapLauncher(ClassLoader cl, ProtectionDomain domain) {
final var cpwClassName = "cpw.mods.bootstraplauncher.BootstrapLauncher";
final var cpwClassFile = "cpw/mods/bootstraplauncher/BootstrapLauncher.class";
System.out.println("Transforming " + cpwClassName);
try(var inputStream = cl.getResourceAsStream(cpwClassFile)) {
if (inputStream == null) {
return new IOException("getResourceAsStream can't read BootstrapLauncher.class");
public byte[] transformBootstrapLauncher(InputStream inputStream) throws IOException {
System.out.println("Transforming cpw.mods.bootstraplauncher.BootstrapLauncher");
var asmClass = new ClassNode();
new ClassReader(inputStream).accept(asmClass, 0);

// Find main(String[])
MethodNode asmMain = null;
for (var asmMethod : asmClass.methods) {
if ("main".equals(asmMethod.name)) {
asmMain = asmMethod;
break;
}
var asmClass = new ClassNode();
new ClassReader(inputStream).accept(asmClass, 0);
// Find main(String[])
MethodNode asmMain = null;
for (var asmMethod : asmClass.methods) {
if ("main".equals(asmMethod.name)) {
asmMain = asmMethod;
}
if (asmMain == null) {
throw new RuntimeException("Cannot find main(String[]) in BootstrapLauncher");
}

// Find Consumer.accept(...)
var insns = asmMain.instructions;
MethodInsnNode injectionPoint = null;
for (int i = 0; i < insns.size(); i++) {
if (insns.get(i) instanceof MethodInsnNode invoke) {
if ("java/util/function/Consumer".equals(invoke.owner)
&& "accept".equals(invoke.name)) {
injectionPoint = invoke;
break;
}
}
if (asmMain == null) {
return new NullPointerException("Cannot find main(String[]) in BootstrapLauncher");
}
// Apply transformation
var insns = asmMain.instructions;
var injected = false;
for (int i = 0; i < insns.size(); i++) {
if (insns.get(i) instanceof MethodInsnNode invoke) {
if ("java/util/function/Consumer".equals(invoke.owner)
&& "accept".equals(invoke.name)) {
// Raw: [SERVICE].accept(args);
// Modified: ((Consumer)new ApplicationBootstrap()).accept(args);
var createArclightBoot = new InsnList();
{
var popArgsThenService = new InsnNode(Opcodes.POP2);
var aloadArgs = new VarInsnNode(Opcodes.ALOAD, 0);
var aloadModuleCl = new VarInsnNode(Opcodes.ALOAD, 15);
var onInvoke = new MethodInsnNode(
Opcodes.INVOKESTATIC,
"io/izzel/arclight/boot/application/BootstrapTransformer",
"onInvoke$BootstrapLauncher",
"([Ljava/lang/String;Lcpw/mods/cl/ModuleClassLoader;)V"
);
createArclightBoot.add(popArgsThenService);
createArclightBoot.add(aloadArgs);
createArclightBoot.add(aloadModuleCl);
createArclightBoot.add(onInvoke);
}
insns.insert(invoke, createArclightBoot);
insns.remove(invoke);
injected = true;
break;
}
}
}
if (!injected) {
return new Exception("BootstrapTransformer failed to transform BootstrapLauncher: Consumer.accept(String[]) not found");
}
// Save and define transformed class
var cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
asmClass.accept(cw);
var bytes = cw.toByteArray();
Unsafe.defineClass(cpwClassName, bytes, 0, bytes.length, cl, domain);
} catch (IOException e) {
return e;
}
return null;
}
if (injectionPoint == null) {
throw new RuntimeException("BootstrapTransformer failed to transform BootstrapLauncher: Consumer.accept(String[]) not found");
}

@SuppressWarnings({"unused", "unchecked"})
public static void onInvoke$BootstrapLauncher(String[] args, ModuleClassLoader moduleCl) {
try {
Class<ApplicationBootstrap> arclightBootClz = (Class<ApplicationBootstrap>) moduleCl.loadClass("io.izzel.arclight.boot.application.ApplicationBootstrap");
Object instance = arclightBootClz.getConstructor().newInstance();
arclightBootClz.getMethod("accept", String[].class).invoke(instance, (Object) args);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
// Apply transformation
// Raw: [SERVICE].accept(args);
// Modified: BootstrapTransformer.onInvoke$BootstrapLauncher(...);
var createArclightBoot = new InsnList();
{
var popArgsThenService = new InsnNode(Opcodes.POP2);
var aloadArgs = new VarInsnNode(Opcodes.ALOAD, 0);
var aloadModuleCl = new VarInsnNode(Opcodes.ALOAD, 15);
var onInvoke = new MethodInsnNode(
Opcodes.INVOKESTATIC,
"io/izzel/arclight/boot/application/BootstrapTransformer",
"onInvoke$BootstrapLauncher",
"([Ljava/lang/String;Lcpw/mods/cl/ModuleClassLoader;)V"
);
createArclightBoot.add(popArgsThenService);
createArclightBoot.add(aloadArgs);
createArclightBoot.add(aloadModuleCl);
createArclightBoot.add(onInvoke);
}
insns.insert(injectionPoint, createArclightBoot);
insns.remove(injectionPoint);

// Save transformed class
var cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
asmClass.accept(cw);
return cw.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.izzel.arclight.boot.application;

import cpw.mods.cl.ModuleClassLoader;

import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
Expand All @@ -24,9 +26,9 @@ public static void main(String[] args) throws Throwable {
// The manifest data will be unavailable for further use, stop here
verifyManifest();
Map.Entry<String, List<String>> install = forgeInstall();
var clazz = Main_Forge.class;
var cl = BootstrapTransformer.loadTransform(install.getKey(), clazz.getClassLoader(), clazz.getProtectionDomain());
var method = cl.getMethod("main", String[].class);
var cl = new BootstrapTransformer(Main_Forge.class.getClassLoader());
var clazz = cl.loadClass(install.getKey(), true);
var method = clazz.getMethod("main", String[].class);
var target = Stream.concat(install.getValue().stream(), Arrays.stream(args)).toArray(String[]::new);
method.invoke(null, (Object) target);
} catch (Exception e) {
Expand Down

0 comments on commit 9352928

Please sign in to comment.