From c1e63675bbc756ed8164ecb7dec39ad5fcdfb3a9 Mon Sep 17 00:00:00 2001 From: InitAuther97 Date: Thu, 23 Jan 2025 21:14:45 +0800 Subject: [PATCH] Avoid UB in installer and check manifest data in advance (#1628) * banned installer that contains broken manifest data from running * use reflection to avoid relying on an undefined order of ServiceLoader.load().stream() * Use custom class loader to replace Unsafe in transforming BootstrapLauncher --- .../arclight/boot/AbstractBootstrap.java | 6 +- .../application/BootstrapTransformer.java | 173 ++++++++++++++++++ .../arclight/boot/application/Main_Forge.java | 27 ++- 3 files changed, 201 insertions(+), 5 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/AbstractBootstrap.java b/arclight-forge/src/main/java/io/izzel/arclight/boot/AbstractBootstrap.java index 1e2e76adf..abbb63c7a 100644 --- a/arclight-forge/src/main/java/io/izzel/arclight/boot/AbstractBootstrap.java +++ b/arclight-forge/src/main/java/io/izzel/arclight/boot/AbstractBootstrap.java @@ -28,7 +28,6 @@ import java.util.Map; import java.util.jar.Attributes; import java.util.jar.Manifest; -import java.util.stream.Collectors; public class AbstractBootstrap { @@ -91,13 +90,14 @@ protected void dirtyHacks() throws Exception { protected void setupMod() throws Exception { ArclightVersion.setVersion(ArclightVersion.TRIALS); + var logger = LogManager.getLogger("Arclight"); try (InputStream stream = getClass().getModule().getResourceAsStream("/META-INF/MANIFEST.MF")) { Manifest manifest = new Manifest(stream); Attributes attributes = manifest.getMainAttributes(); String version = attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION); extract(getClass().getModule().getResourceAsStream("/common.jar"), version); String buildTime = attributes.getValue("Implementation-Timestamp"); - LogManager.getLogger("Arclight").info(ArclightLocale.getInstance().get("logo"), + logger.info(ArclightLocale.getInstance().get("logo"), ArclightLocale.getInstance().get("release-name." + ArclightVersion.current().getReleaseName()), version, buildTime); } } @@ -110,7 +110,7 @@ private void extract(InputStream path, String version) throws Exception { } var mod = dir.resolve(version + ".jar"); if (!Files.exists(mod) || Boolean.getBoolean("arclight.alwaysExtract")) { - for (Path old : Files.list(dir).collect(Collectors.toList())) { + for (Path old : Files.list(dir).toList()) { Files.delete(old); } Files.copy(path, mod); 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 000000000..9e7dcbb34 --- /dev/null +++ b/arclight-forge/src/main/java/io/izzel/arclight/boot/application/BootstrapTransformer.java @@ -0,0 +1,173 @@ +package io.izzel.arclight.boot.application; + +import cpw.mods.cl.ModuleClassLoader; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; + +import java.io.IOException; +import java.io.InputStream; +import java.security.ProtectionDomain; + +/* + * 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); + } + } + throw new UnsupportedOperationException("Transformation for " + className + " is not supported"); + } + + /* + * 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 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; + } + } + 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 (injectionPoint == null) { + throw new RuntimeException("BootstrapTransformer failed to transform BootstrapLauncher: Consumer.accept(String[]) not found"); + } + + // 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 2fe463362..ae4971358 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,7 +1,12 @@ package io.izzel.arclight.boot.application; +import cpw.mods.cl.ModuleClassLoader; + +import java.io.File; +import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; @@ -10,15 +15,20 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.jar.JarFile; import java.util.stream.Stream; public class Main_Forge { public static void main(String[] args) throws Throwable { try { + // Modifying the installer with a file archiver will corrupt the jar file + // The manifest data will be unavailable for further use, stop here + verifyManifest(); Map.Entry> install = forgeInstall(); - var cl = Class.forName(install.getKey()); - 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) { @@ -28,6 +38,19 @@ public static void main(String[] args) throws Throwable { } } + 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.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"); + } + } + } + @SuppressWarnings("unchecked") private static Map.Entry> forgeInstall() throws Throwable { var path = Paths.get(".arclight", "gson.jar");