From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Sun, 28 Apr 2024 11:12:14 -0700 Subject: [PATCH] Modify library loader jars bytecode diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..405416dc3d1c8c58b4e0c880d8751ca319188f62 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java @@ -0,0 +1,185 @@ +package io.papermc.paper.plugin.entrypoint.classloader; + +import io.papermc.paper.pluginremap.reflect.ReflectionRemapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSigner; +import java.security.CodeSource; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import static java.util.Objects.requireNonNullElse; + +public final class BytecodeModifyingURLClassLoader extends URLClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + private static final Object MISSING_MANIFEST = new Object(); + + private final Function modifier; + private final Map manifests = new ConcurrentHashMap<>(); + + public BytecodeModifyingURLClassLoader( + final URL[] urls, + final ClassLoader parent, + final Function modifier + ) { + super(urls, parent); + this.modifier = modifier; + } + + public BytecodeModifyingURLClassLoader( + final URL[] urls, + final ClassLoader parent + ) { + this(urls, parent, bytes -> { + final ClassReader classReader = new ClassReader(bytes); + final ClassWriter classWriter = new ClassWriter(classReader, 0); + final ClassVisitor visitor = ReflectionRemapper.visitor(classWriter); + if (visitor == classWriter) { + return bytes; + } + classReader.accept(visitor, 0); + return classWriter.toByteArray(); + }); + } + + @Override + protected Class findClass(final String name) throws ClassNotFoundException { + final Class result; + final String path = name.replace('.', '/').concat(".class"); + final URL url = this.findResource(path); + if (url != null) { + try { + result = this.defineClass(name, url); + } catch (final IOException e) { + throw new ClassNotFoundException(name, e); + } + } else { + result = null; + } + if (result == null) { + throw new ClassNotFoundException(name); + } + return result; + } + + private Class defineClass(String name, URL url) throws IOException { + int i = name.lastIndexOf('.'); + if (i != -1) { + String pkgname = name.substring(0, i); + // Check if package already loaded. + final @Nullable Manifest man = this.manifestFor(url); + if (this.getAndVerifyPackage(pkgname, man, url) == null) { + try { + if (man != null) { + this.definePackage(pkgname, man, url); + } else { + this.definePackage(pkgname, null, null, null, null, null, null, null); + } + } catch (IllegalArgumentException iae) { + // parallel-capable class loaders: re-verify in case of a + // race condition + if (this.getAndVerifyPackage(pkgname, man, url) == null) { + // Should never happen + throw new AssertionError("Cannot find package " + + pkgname); + } + } + } + } + final byte[] bytes; + try (final InputStream is = url.openStream()) { + bytes = is.readAllBytes(); + } + + final byte[] modified = this.modifier.apply(bytes); + + final CodeSource cs = new CodeSource(url, (CodeSigner[]) null); + return this.defineClass(name, modified, 0, modified.length, cs); + } + + private Package getAndVerifyPackage( + String pkgname, + Manifest man, URL url + ) { + Package pkg = getDefinedPackage(pkgname); + if (pkg != null) { + // Package found, so check package sealing. + if (pkg.isSealed()) { + // Verify that code source URL is the same. + if (!pkg.isSealed(url)) { + throw new SecurityException( + "sealing violation: package " + pkgname + " is sealed"); + } + } else { + // Make sure we are not attempting to seal the package + // at this code source URL. + if ((man != null) && this.isSealed(pkgname, man)) { + throw new SecurityException( + "sealing violation: can't seal package " + pkgname + + ": already loaded"); + } + } + } + return pkg; + } + + private boolean isSealed(String name, Manifest man) { + Attributes attr = man.getAttributes(name.replace('.', '/').concat("/")); + String sealed = null; + if (attr != null) { + sealed = attr.getValue(Attributes.Name.SEALED); + } + if (sealed == null) { + if ((attr = man.getMainAttributes()) != null) { + sealed = attr.getValue(Attributes.Name.SEALED); + } + } + return "true".equalsIgnoreCase(sealed); + } + + private @Nullable Manifest manifestFor(final URL url) throws IOException { + Manifest man = null; + if (url.getProtocol().equals("jar")) { + try { + final Object computedManifest = this.manifests.computeIfAbsent(jarName(url), $ -> { + try { + final Manifest m = ((JarURLConnection) url.openConnection()).getManifest(); + return requireNonNullElse(m, MISSING_MANIFEST); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }); + if (computedManifest instanceof Manifest found) { + man = found; + } + } catch (final UncheckedIOException e) { + throw e.getCause(); + } catch (final IllegalArgumentException e) { + throw new IOException(e); + } + } + return man; + } + + private static String jarName(final URL sourceUrl) { + final int exclamationIdx = sourceUrl.getPath().lastIndexOf('!'); + if (exclamationIdx != -1) { + return sourceUrl.getPath().substring(0, exclamationIdx); + } + throw new IllegalArgumentException("Could not find jar for URL " + sourceUrl); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java index f38ecd7f65dc24e4a3f0bc675e3730287ac353f1..ca6cb891e9da9d7e08f1a82fab212d2063cc9ef6 100644 --- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java +++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java @@ -1,12 +1,11 @@ package io.papermc.paper.plugin.loader; import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader; +import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; import io.papermc.paper.plugin.loader.library.ClassPathLibrary; import io.papermc.paper.plugin.loader.library.PaperLibraryStore; -import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; -import org.jetbrains.annotations.NotNull; - import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -16,6 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.jar.JarFile; import java.util.logging.Logger; +import org.jetbrains.annotations.NotNull; public class PaperClasspathBuilder implements PluginClasspathBuilder { @@ -56,7 +56,8 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder { } try { - return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader())); + final URLClassLoader libraryLoader = new BytecodeModifyingURLClassLoader(urls, this.getClass().getClassLoader()); + return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), libraryLoader); } catch (IOException exception) { throw new RuntimeException(exception); } diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java index bdd9bc8a414719b9f1d6f01f90539ddb8603a878..31f05a7336ea124d24a5059652a2950a9f672758 100644 --- a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java +++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java @@ -1,9 +1,11 @@ package io.papermc.paper.plugin.provider.type.spigot; +import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader; import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints; import io.papermc.paper.plugin.provider.type.PluginTypeFactory; import org.bukkit.plugin.InvalidDescriptionException; import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.LibraryLoader; import org.yaml.snakeyaml.error.YAMLException; import java.io.IOException; @@ -15,6 +17,10 @@ import java.util.jar.JarFile; class SpigotPluginProviderFactory implements PluginTypeFactory { + static { + LibraryLoader.LIBRARY_LOADER_FACTORY = BytecodeModifyingURLClassLoader::new; + } + @Override public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException { // Copied from SimplePluginManager#loadPlugins