From 32ad479664d8eb0d3204608b90ed8a561982b8bf Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sun, 28 Apr 2024 13:14:10 -0700
Subject: [PATCH] Rewrite reflection in library loader jars (#10608)

* Rewrite reflection in library loader jars

* Address todos
---
 ...difying-library-loader-jars-bytecode.patch |  34 +++
 .../Modify-library-loader-jars-bytecode.patch | 261 ++++++++++++++++++
 2 files changed, 295 insertions(+)
 create mode 100644 patches/api/Allow-modifying-library-loader-jars-bytecode.patch
 create mode 100644 patches/server/Modify-library-loader-jars-bytecode.patch

diff --git a/patches/api/Allow-modifying-library-loader-jars-bytecode.patch b/patches/api/Allow-modifying-library-loader-jars-bytecode.patch
new file mode 100644
index 0000000000..bd3cb0c1eb
--- /dev/null
+++ b/patches/api/Allow-modifying-library-loader-jars-bytecode.patch
@@ -0,0 +1,34 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
+Date: Sun, 28 Apr 2024 11:11:26 -0700
+Subject: [PATCH] Allow modifying library loader jars bytecode
+
+
+diff --git a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
++++ b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
+@@ -0,0 +0,0 @@ public class LibraryLoader
+     private final RepositorySystem repository;
+     private final DefaultRepositorySystemSession session;
+     private final List<RemoteRepository> repositories;
++    public static java.util.function.BiFunction<URL[], ClassLoader, URLClassLoader> LIBRARY_LOADER_FACTORY; // Paper - rewrite reflection in libraries
+ 
+     public LibraryLoader(@NotNull Logger logger)
+     {
+@@ -0,0 +0,0 @@ public class LibraryLoader
+             } );
+         }
+ 
+-        URLClassLoader loader = new URLClassLoader( jarFiles.toArray( new URL[ jarFiles.size() ] ), getClass().getClassLoader() );
++        // Paper start - rewrite reflection in libraries
++        URLClassLoader loader;
++        if (LIBRARY_LOADER_FACTORY == null) {
++            loader = new URLClassLoader( jarFiles.toArray( new URL[ jarFiles.size() ] ), getClass().getClassLoader() );
++        } else {
++            loader = LIBRARY_LOADER_FACTORY.apply(jarFiles.toArray( new URL[ jarFiles.size() ] ), getClass().getClassLoader());
++        }
++        // Paper end - rewrite reflection in libraries
+ 
+         return loader;
+     }
diff --git a/patches/server/Modify-library-loader-jars-bytecode.patch b/patches/server/Modify-library-loader-jars-bytecode.patch
new file mode 100644
index 0000000000..b8012ea193
--- /dev/null
+++ b/patches/server/Modify-library-loader-jars-bytecode.patch
@@ -0,0 +1,261 @@
+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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
+@@ -0,0 +0,0 @@
++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<byte[], byte[]> modifier;
++    private final Map<String, Object> manifests = new ConcurrentHashMap<>();
++
++    public BytecodeModifyingURLClassLoader(
++        final URL[] urls,
++        final ClassLoader parent,
++        final Function<byte[], byte[]> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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 {
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ import java.util.jar.JarFile;
+ 
+ class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
+ 
++    static {
++        LibraryLoader.LIBRARY_LOADER_FACTORY = BytecodeModifyingURLClassLoader::new;
++    }
++
+     @Override
+     public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException {
+         // Copied from SimplePluginManager#loadPlugins