From 9352928f79a626ef5e375179ce8f45aba63fe89e Mon Sep 17 00:00:00 2001 From: InitAuther97 Date: Tue, 21 Jan 2025 19:02:10 +0800 Subject: [PATCH] Use custom class loader to replace Unsafe in transforming BootstrapLauncher --- .../application/BootstrapTransformer.java | 232 +++++++++++------- .../arclight/boot/application/Main_Forge.java | 8 +- 2 files changed, 152 insertions(+), 88 deletions(-) diff --git a/arclight-forge/src/main/java/io/izzel/arclight/boot/application/BootstrapTransformer.java b/arclight-forge/src/main/java/io/izzel/arclight/boot/application/BootstrapTransformer.java index 5fa2167d..9e7dcbb3 100644 --- a/arclight-forge/src/main/java/io/izzel/arclight/boot/application/BootstrapTransformer.java +++ b/arclight-forge/src/main/java/io/izzel/arclight/boot/application/BootstrapTransformer.java @@ -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> 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 arclightBootClz = (Class) 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"); } /* @@ -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 arclightBootClz = (Class) 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(); } } diff --git a/arclight-forge/src/main/java/io/izzel/arclight/boot/application/Main_Forge.java b/arclight-forge/src/main/java/io/izzel/arclight/boot/application/Main_Forge.java index a87aee6c..ae497135 100644 --- a/arclight-forge/src/main/java/io/izzel/arclight/boot/application/Main_Forge.java +++ b/arclight-forge/src/main/java/io/izzel/arclight/boot/application/Main_Forge.java @@ -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; @@ -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> 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) {