From 95832d637da7039ac9e917252afc7b6be5b2b2f9 Mon Sep 17 00:00:00 2001 From: InitAuther97 Date: Mon, 20 Jan 2025 23:37:14 +0800 Subject: [PATCH] use reflection to avoid relying on an undefined order of ServiceLoader.load().stream() --- .../application/BootstrapTransformer.java | 111 ++++++++++++++++++ .../arclight/boot/application/Main_Forge.java | 7 +- 2 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 arclight-forge/src/main/java/io/izzel/arclight/boot/application/BootstrapTransformer.java 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 new file mode 100644 index 00000000..5fa2167d --- /dev/null +++ b/arclight-forge/src/main/java/io/izzel/arclight/boot/application/BootstrapTransformer.java @@ -0,0 +1,111 @@ +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.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"); + } + var ex = SUPPORTED.get(className).apply(cl, domain); + if (ex != null) throw ex; + return cl.loadClass(className); + } + + /* + * Previous implementation of BootstrapLauncher relies on the order of ServiceLoader.load().stream() + * where the ApplicationBootstrap will be ahead of modlauncher, which is an UB related to module name. + * 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"); + } + 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; + } + } + 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; + } + + @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); + } + } +} 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 f8c7cff0..a87aee6c 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 @@ -24,7 +24,8 @@ 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 cl = Class.forName(install.getKey()); + var clazz = Main_Forge.class; + var cl = BootstrapTransformer.loadTransform(install.getKey(), clazz.getClassLoader(), clazz.getProtectionDomain()); var method = cl.getMethod("main", String[].class); var target = Stream.concat(install.getValue().stream(), Arrays.stream(args)).toArray(String[]::new); method.invoke(null, (Object) target); @@ -39,13 +40,13 @@ private static void verifyManifest() throws IOException, URISyntaxException { var location = Main_Forge.class.getProtectionDomain().getCodeSource().getLocation(); try(JarFile baseArchive = new JarFile(new File(location.toURI()))) { var mf = baseArchive.getManifest(); - if (mf == null || mf.getEntries().isEmpty()) { + if (mf == null || mf.getMainAttributes().isEmpty()) { System.err.println("Failed to verify completeness for Arclight installer."); System.err.println("The manifest data is corrupted, is the jar file modified?"); System.err.println("Cannot proceed, Arclight will exit"); + throw new IOException("The installer jar file is corrupted"); } } - throw new IOException("The installer jar file is corrupted"); } @SuppressWarnings("unchecked")