From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Nassim Jahnke <nassim@njahnke.dev>
Date: Sun, 30 Oct 2022 23:47:26 +0100
Subject: [PATCH] Remap reflection calls in plugins using internals

Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com>

diff --git a/build.gradle.kts b/build.gradle.kts
index 8350fc099b68918fb03a21b6a5047ceee72dcbb4..404c2eefa88319e0eaf7b0d1d2696c91dd0e0e0b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -47,6 +47,12 @@ dependencies {
     testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
     implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling
     implementation("net.neoforged:AutoRenamingTool:2.0.3") // Paper - remap plugins
+    // Paper start - Remap reflection
+    val reflectionRewriterVersion = "0.0.3"
+    implementation("io.papermc:reflection-rewriter:$reflectionRewriterVersion")
+    implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion")
+    implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion")
+    // Paper end - Remap reflection
 }
 
 paperweight {
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
index 893ad5e7c2d32ccd64962d95d146bbd317c28ab8..3d73ea0e63c97b2b08e719b7be7af3894fb2d4e8 100644
--- a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
+++ b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableBiMap;
 import com.mojang.logging.LogUtils;
 import io.leangen.geantyref.TypeToken;
 import io.papermc.paper.configuration.serializer.collections.MapSerializer;
+import io.papermc.paper.util.MappingEnvironment;
 import io.papermc.paper.util.ObfHelper;
 import net.minecraft.network.protocol.Packet;
 import org.checkerframework.checker.nullness.qual.Nullable;
@@ -69,7 +70,7 @@ public final class PacketClassSerializer extends ScalarSerializer<Class<? extend
     @Override
     protected @Nullable Object serialize(final Class<? extends Packet<?>> packetClass, final Predicate<Class<?>> typeSupported) {
         final String name = packetClass.getName();
-        @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server
+        @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null || !MappingEnvironment.reobf() ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server
         if (mojName == null && MOJANG_TO_OBF.containsKey(name)) {
             mojName = name;
         }
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..1240c061c121e8d5eb9add4e5e21955ee6df9368
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
@@ -0,0 +1,187 @@
+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.URI;
+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);
+            final URL jarUrl = URI.create(jarName(url)).toURL();
+            if (this.getAndVerifyPackage(pkgname, man, jarUrl) == null) {
+                try {
+                    if (man != null) {
+                        this.definePackage(pkgname, man, jarUrl);
+                    } 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, jarUrl) == 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/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
index f9a2c55a354c877749db3f92956de802ae575788..39182cdd17473da0123dc7172dce507eab29fedc 100644
--- a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
@@ -1,12 +1,17 @@
 package io.papermc.paper.plugin.entrypoint.classloader;
 
 import io.papermc.paper.plugin.configuration.PluginMeta;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
 
 // Stub, implement in future.
 public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {
 
     @Override
     public byte[] modify(PluginMeta configuration, byte[] bytecode) {
-        return bytecode;
+        ClassReader classReader = new ClassReader(bytecode);
+        ClassWriter classWriter = new ClassWriter(classReader, 0);
+        classReader.accept(io.papermc.paper.pluginremap.reflect.ReflectionRemapper.visitor(classWriter), 0);
+        return classWriter.toByteArray();
     }
 }
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 f576060c8fe872772bbafe2016fc9b83a3c095f1..f9d4b33050a6fe8c2dabe8e5eec075d95dc513e0 100644
--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
@@ -2,12 +2,12 @@ package io.papermc.paper.plugin.loader;
 
 import io.papermc.paper.plugin.PluginInitializerManager;
 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 io.papermc.paper.util.MappingEnvironment;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -17,6 +17,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 {
 
@@ -60,7 +61,10 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder {
         }
 
         try {
-            return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader()));
+            final URLClassLoader libraryLoader = MappingEnvironment.DISABLE_PLUGIN_REWRITING
+                ? new URLClassLoader(urls, this.getClass().getClassLoader())
+                : 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..fdb52ad85cfaa1d53aadcad72cec3d3c8c12c058 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,12 @@
 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 io.papermc.paper.util.MappingEnvironment;
 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 +18,12 @@ import java.util.jar.JarFile;
 
 class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
 
+    static {
+        if (!MappingEnvironment.DISABLE_PLUGIN_REWRITING) {
+			LibraryLoader.LIBRARY_LOADER_FACTORY = BytecodeModifyingURLClassLoader::new;
+		}
+    }
+
     @Override
     public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException {
         // Copied from SimplePluginManager#loadPlugins
diff --git a/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java
new file mode 100644
index 0000000000000000000000000000000000000000..795d79bcfc4177edfcb12cf9db392780c3f04aa7
--- /dev/null
+++ b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java
@@ -0,0 +1,216 @@
+package io.papermc.paper.pluginremap.reflect;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.util.MappingEnvironment;
+import io.papermc.paper.util.ObfHelper;
+import io.papermc.reflectionrewriter.runtime.AbstractDefaultRulesReflectionProxy;
+import io.papermc.reflectionrewriter.runtime.DefineClassReflectionProxy;
+import java.lang.invoke.MethodHandles;
+import java.nio.ByteBuffer;
+import java.security.CodeSource;
+import java.security.ProtectionDomain;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.slf4j.Logger;
+
+// todo proper inheritance handling
+@SuppressWarnings("unused")
+@DefaultQualifier(NonNull.class)
+public final class PaperReflection extends AbstractDefaultRulesReflectionProxy implements DefineClassReflectionProxy {
+    // concat to avoid being rewritten by shadow
+    private static final Logger LOGGER = LogUtils.getLogger();
+    private static final String CB_PACKAGE_PREFIX = "org.bukkit.".concat("craftbukkit.");
+    private static final String LEGACY_CB_PACKAGE_PREFIX = "org.bukkit.".concat("craftbukkit.") + MappingEnvironment.LEGACY_CB_VERSION + ".";
+
+    private final DefineClassReflectionProxy defineClassProxy;
+    private final Map<String, ObfHelper.ClassMapping> mappingsByMojangName;
+    private final Map<String, ObfHelper.ClassMapping> mappingsByObfName;
+    // Reflection does not care about method return values, so this map removes the return value descriptor from the key
+    private final Map<String, Map<String, String>> strippedMethodMappings;
+
+    PaperReflection() {
+        this.defineClassProxy = DefineClassReflectionProxy.create(PaperReflection::processClass);
+        if (!MappingEnvironment.hasMappings()) {
+            this.mappingsByMojangName = Map.of();
+            this.mappingsByObfName = Map.of();
+            this.strippedMethodMappings = Map.of();
+            return;
+        }
+        final ObfHelper obfHelper = ObfHelper.INSTANCE;
+        this.mappingsByMojangName = Objects.requireNonNull(obfHelper.mappingsByMojangName(), "mappingsByMojangName");
+        this.mappingsByObfName = Objects.requireNonNull(obfHelper.mappingsByObfName(), "mappingsByObfName");
+        this.strippedMethodMappings = this.mappingsByMojangName.entrySet().stream().collect(Collectors.toUnmodifiableMap(
+            Map.Entry::getKey,
+            entry -> entry.getValue().strippedMethods()
+        ));
+    }
+
+    @Override
+    protected String mapClassName(final String name) {
+        final ObfHelper.@Nullable ClassMapping mapping = this.mappingsByObfName.get(name);
+        return mapping != null ? mapping.mojangName() : removeCraftBukkitRelocation(name);
+    }
+
+    @Override
+    protected String mapDeclaredMethodName(final Class<?> clazz, final String name, final Class<?> @Nullable ... parameterTypes) {
+        final @Nullable Map<String, String> mapping = this.strippedMethodMappings.get(clazz.getName());
+        if (mapping == null) {
+            return name;
+        }
+        return mapping.getOrDefault(strippedMethodKey(name, parameterTypes), name);
+    }
+
+    @Override
+    protected String mapMethodName(final Class<?> clazz, final String name, final Class<?> @Nullable ... parameterTypes) {
+        final @Nullable String mapped = this.findMappedMethodName(clazz, name, parameterTypes);
+        return mapped != null ? mapped : name;
+    }
+
+    @Override
+    protected String mapDeclaredFieldName(final Class<?> clazz, final String name) {
+        final ObfHelper.@Nullable ClassMapping mapping = this.mappingsByMojangName.get(clazz.getName());
+        if (mapping == null) {
+            return name;
+        }
+        return mapping.fieldsByObf().getOrDefault(name, name);
+    }
+
+    @Override
+    protected String mapFieldName(final Class<?> clazz, final String name) {
+        final @Nullable String mapped = this.findMappedFieldName(clazz, name);
+        return mapped != null ? mapped : name;
+    }
+
+    private @Nullable String findMappedMethodName(final Class<?> clazz, final String name, final Class<?> @Nullable ... parameterTypes) {
+        final Map<String, String> map = this.strippedMethodMappings.get(clazz.getName());
+        @Nullable String mapped = null;
+        if (map != null) {
+            mapped = map.get(strippedMethodKey(name, parameterTypes));
+            if (mapped != null) {
+                return mapped;
+            }
+        }
+        // JVM checks super before interfaces
+        final Class<?> superClass = clazz.getSuperclass();
+        if (superClass != null) {
+            mapped = this.findMappedMethodName(superClass, name, parameterTypes);
+        }
+        if (mapped == null) {
+            for (final Class<?> i : clazz.getInterfaces()) {
+                mapped = this.findMappedMethodName(i, name, parameterTypes);
+                if (mapped != null) {
+                    break;
+                }
+            }
+        }
+        return mapped;
+    }
+
+    private @Nullable String findMappedFieldName(final Class<?> clazz, final String name) {
+        final ObfHelper.ClassMapping mapping = this.mappingsByMojangName.get(clazz.getName());
+        @Nullable String mapped = null;
+        if (mapping != null) {
+            mapped = mapping.fieldsByObf().get(name);
+            if (mapped != null) {
+                return mapped;
+            }
+        }
+        // The JVM checks super before interfaces
+        final Class<?> superClass = clazz.getSuperclass();
+        if (superClass != null) {
+            mapped = this.findMappedFieldName(superClass, name);
+        }
+        if (mapped == null) {
+            for (final Class<?> i : clazz.getInterfaces()) {
+                mapped = this.findMappedFieldName(i, name);
+                if (mapped != null) {
+                    break;
+                }
+            }
+        }
+        return mapped;
+    }
+
+    private static String strippedMethodKey(final String methodName, final Class<?> @Nullable ... parameterTypes) {
+        return methodName + parameterDescriptor(parameterTypes);
+    }
+
+    private static String parameterDescriptor(final Class<?> @Nullable ... parameterTypes) {
+        if (parameterTypes == null) {
+            // Null parameterTypes is treated as an empty array
+            return "()";
+        }
+        final StringBuilder builder = new StringBuilder();
+        builder.append('(');
+        for (final Class<?> parameterType : parameterTypes) {
+            builder.append(parameterType.descriptorString());
+        }
+        builder.append(')');
+        return builder.toString();
+    }
+
+    private static String removeCraftBukkitRelocation(final String name) {
+        if (MappingEnvironment.hasMappings()) {
+            // Relocation is applied in reobf, and when mappings are present they handle the relocation
+            return name;
+        }
+        if (name.startsWith(LEGACY_CB_PACKAGE_PREFIX)) {
+            return CB_PACKAGE_PREFIX + name.substring(LEGACY_CB_PACKAGE_PREFIX.length());
+        }
+        return name;
+    }
+
+    @Override
+    public Class<?> defineClass(final Object loader, final byte[] b, final int off, final int len) throws ClassFormatError {
+        return this.defineClassProxy.defineClass(loader, b, off, len);
+    }
+
+    @Override
+    public Class<?> defineClass(final Object loader, final String name, final byte[] b, final int off, final int len) throws ClassFormatError {
+        return this.defineClassProxy.defineClass(loader, name, b, off, len);
+    }
+
+    @Override
+    public Class<?> defineClass(final Object loader, final @Nullable String name, final byte[] b, final int off, final int len, final @Nullable ProtectionDomain protectionDomain) throws ClassFormatError {
+        return this.defineClassProxy.defineClass(loader, name, b, off, len, protectionDomain);
+    }
+
+    @Override
+    public Class<?> defineClass(final Object loader, final String name, final ByteBuffer b, final ProtectionDomain protectionDomain) throws ClassFormatError {
+        return this.defineClassProxy.defineClass(loader, name, b, protectionDomain);
+    }
+
+    @Override
+    public Class<?> defineClass(final Object secureLoader, final String name, final byte[] b, final int off, final int len, final CodeSource cs) {
+        return this.defineClassProxy.defineClass(secureLoader, name, b, off, len, cs);
+    }
+
+    @Override
+    public Class<?> defineClass(final Object secureLoader, final String name, final ByteBuffer b, final CodeSource cs) {
+        return this.defineClassProxy.defineClass(secureLoader, name, b, cs);
+    }
+
+    @Override
+    public Class<?> defineClass(final MethodHandles.Lookup lookup, final byte[] bytes) throws IllegalAccessException {
+        return this.defineClassProxy.defineClass(lookup, bytes);
+    }
+
+    // todo apply bytecode remap here as well
+    private static byte[] processClass(final byte[] bytes) {
+        try {
+            final ClassReader reader = new ClassReader(bytes);
+            final ClassWriter writer = new ClassWriter(reader, 0);
+            reader.accept(ReflectionRemapper.visitor(writer), 0);
+            return writer.toByteArray();
+        } catch (final Exception ex) {
+            LOGGER.warn("Failed to process class bytes", ex);
+            return bytes;
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java b/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..5fa5868e82d1f00498d0c5771369e1718b2df4ee
--- /dev/null
+++ b/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java
@@ -0,0 +1,54 @@
+package io.papermc.paper.pluginremap.reflect;
+
+import io.papermc.asm.ClassInfoProvider;
+import io.papermc.asm.RewriteRuleVisitorFactory;
+import io.papermc.paper.util.MappingEnvironment;
+import io.papermc.reflectionrewriter.BaseReflectionRules;
+import io.papermc.reflectionrewriter.DefineClassRule;
+import io.papermc.reflectionrewriter.proxygenerator.ProxyGenerator;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+
+@DefaultQualifier(NonNull.class)
+public final class ReflectionRemapper {
+    private static final String PAPER_REFLECTION_HOLDER = "io.papermc.paper.pluginremap.reflect.PaperReflectionHolder";
+    private static final String PAPER_REFLECTION_HOLDER_DESC = PAPER_REFLECTION_HOLDER.replace('.', '/');
+    private static final RewriteRuleVisitorFactory VISITOR_FACTORY = RewriteRuleVisitorFactory.create(
+        Opcodes.ASM9,
+        chain -> chain.then(new BaseReflectionRules(PAPER_REFLECTION_HOLDER).rules())
+            .then(DefineClassRule.create(PAPER_REFLECTION_HOLDER_DESC, true)),
+        ClassInfoProvider.basic()
+    );
+
+    static {
+        if (!MappingEnvironment.reobf()) {
+            setupProxy();
+        }
+    }
+
+    private ReflectionRemapper() {
+    }
+
+    public static ClassVisitor visitor(final ClassVisitor parent) {
+        if (MappingEnvironment.reobf()) {
+            return parent;
+        }
+        return VISITOR_FACTORY.createVisitor(parent);
+    }
+
+    private static void setupProxy() {
+        try {
+            final byte[] bytes = ProxyGenerator.generateProxy(PaperReflection.class, PAPER_REFLECTION_HOLDER_DESC);
+            final MethodHandles.Lookup lookup = MethodHandles.lookup();
+            final Class<?> generated = lookup.defineClass(bytes);
+            final Method init = generated.getDeclaredMethod("init", PaperReflection.class);
+            init.invoke(null, new PaperReflection());
+        } catch (final ReflectiveOperationException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/util/MappingEnvironment.java b/src/main/java/io/papermc/paper/util/MappingEnvironment.java
index 8e4229634d41a42b3d93948eebb77def7c0c72b1..2458685e7612040cea9d638dd2d945da1399ed77 100644
--- a/src/main/java/io/papermc/paper/util/MappingEnvironment.java
+++ b/src/main/java/io/papermc/paper/util/MappingEnvironment.java
@@ -10,6 +10,8 @@ import org.checkerframework.framework.qual.DefaultQualifier;
 
 @DefaultQualifier(NonNull.class)
 public final class MappingEnvironment {
+    public static final boolean DISABLE_PLUGIN_REWRITING = Boolean.getBoolean("paper.disable-plugin-rewriting");
+    public static final String LEGACY_CB_VERSION = "v1_21_R1";
     private static final @Nullable String MAPPINGS_HASH = readMappingsHash();
     private static final boolean REOBF = checkReobf();
 
diff --git a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
index 242811578a786e3807a1a7019d472d5a68f87116..0b65fdf53124f3dd042b2363b1b8df8e1ca7de00 100644
--- a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
+++ b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
@@ -29,6 +29,9 @@ public enum StacktraceDeobfuscator {
     });
 
     public void deobfuscateThrowable(final Throwable throwable) {
+        if (!MappingEnvironment.reobf()) {
+            return;
+        }
         if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
             return;
         }
@@ -44,6 +47,9 @@ public enum StacktraceDeobfuscator {
     }
 
     public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) {
+        if (!MappingEnvironment.reobf()) {
+            return traceElements;
+        }
         if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
             return traceElements;
         }
diff --git a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
index 421ddf6ca955215dff77655a7eda62eb9d90aa92..1cef3614e81e751c98b504c26da4718dc88746df 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
@@ -102,36 +102,26 @@ public class Commodore {
     private static final Map<String, RerouteMethodData> ENUM_METHOD_REROUTE = Commodore.createReroutes(EnumEvil.class);
 
     // Paper start - Plugin rewrites
-    private static final Map<String, String> SEARCH_AND_REMOVE = initReplacementsMap();
-    private static Map<String, String> initReplacementsMap() {
-        Map<String, String> getAndRemove = new HashMap<>();
-        // Be wary of maven shade's relocations
-
-        final java.util.jar.Manifest manifest = io.papermc.paper.util.JarManifests.manifest(Commodore.class);
-        if (Boolean.getBoolean( "debug.rewriteForIde") && manifest != null)
-        {
-            // unversion incoming calls for pre-relocate debug work
-            final String NMS_REVISION_PACKAGE = "v" + manifest.getMainAttributes().getValue("CraftBukkit-Package-Version") + "/";
-
-            getAndRemove.put("org/bukkit/".concat("craftbukkit/" + NMS_REVISION_PACKAGE), NMS_REVISION_PACKAGE);
+    private static final String CB_PACKAGE_PREFIX = "org/bukkit/".concat("craftbukkit/");
+    private static final String LEGACY_CB_PACKAGE_PREFIX = CB_PACKAGE_PREFIX + io.papermc.paper.util.MappingEnvironment.LEGACY_CB_VERSION + "/";
+    private static String runtimeCbPkgPrefix() {
+        if (io.papermc.paper.util.MappingEnvironment.reobf()) {
+            return LEGACY_CB_PACKAGE_PREFIX;
         }
-
-        return getAndRemove;
+        return CB_PACKAGE_PREFIX;
     }
 
     @Nonnull
     private static String getOriginalOrRewrite(@Nonnull String original)
     {
-        String rewrite = null;
-        for ( Map.Entry<String, String> entry : SEARCH_AND_REMOVE.entrySet() )
-        {
-            if ( original.contains( entry.getKey() ) )
-            {
-                rewrite = original.replace( entry.getValue(), "" );
+        // Relocation is applied in reobf, and when mappings are present they handle the relocation
+        if (!io.papermc.paper.util.MappingEnvironment.reobf() && !io.papermc.paper.util.MappingEnvironment.hasMappings()) {
+            if (original.contains(LEGACY_CB_PACKAGE_PREFIX)) {
+                original = original.replace(LEGACY_CB_PACKAGE_PREFIX, CB_PACKAGE_PREFIX);
             }
         }
 
-        return rewrite != null ? rewrite : original;
+        return original;
     }
     // Paper end - Plugin rewrites
 
@@ -214,7 +204,7 @@ public class Commodore {
             visitor = new LimitedClassRemapper(cw, new SimpleRemapper(Commodore.ENUM_RENAMES));
         }
 
-        cr.accept(new ClassRemapper(new ClassVisitor(Opcodes.ASM9, visitor) {
+        cr.accept(new ClassRemapper(new ClassVisitor(Opcodes.ASM9, io.papermc.paper.pluginremap.reflect.ReflectionRemapper.visitor(visitor)) { // Paper
             final Set<RerouteMethodData> rerouteMethodData = new HashSet<>();
             String className;
             boolean isInterface;
diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
index e8205646dbe7088e6bdece2567151bd7f64f36cb..10b5fcf6575255000e52166eb73b60f6a3f88e84 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
@@ -357,7 +357,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
             throw new InvalidPluginException("Plugin API version " + pdf.getAPIVersion() + " is lower than the minimum allowed version. Please update or replace it.");
         }
 
-        if (toCheck.isOlderThan(ApiVersion.FLATTENING)) {
+        if (!io.papermc.paper.util.MappingEnvironment.DISABLE_PLUGIN_REWRITING && toCheck.isOlderThan(ApiVersion.FLATTENING)) { // Paper
             CraftLegacy.init();
         }
 
@@ -372,6 +372,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
 
     @Override
     public byte[] processClass(PluginDescriptionFile pdf, String path, byte[] clazz) {
+        if (io.papermc.paper.util.MappingEnvironment.DISABLE_PLUGIN_REWRITING) return clazz; // Paper
         try {
             clazz = Commodore.convert(clazz, pdf.getName(), ApiVersion.getOrCreateVersion(pdf.getAPIVersion()), ((CraftServer) Bukkit.getServer()).activeCompatibilities);
         } catch (Exception ex) {