Skip to content

Commit

Permalink
Avoid UB in installer and check manifest data in advance (#1628)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
InitAuther97 authored Jan 23, 2025
1 parent 4e94eb7 commit c1e6367
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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);
}
}
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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, List<String>> 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) {
Expand All @@ -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<String, List<String>> forgeInstall() throws Throwable {
var path = Paths.get(".arclight", "gson.jar");
Expand Down

0 comments on commit c1e6367

Please sign in to comment.