aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch')
-rw-r--r--patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch740
1 files changed, 740 insertions, 0 deletions
diff --git a/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch b/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch
new file mode 100644
index 0000000000..ebcc5735fb
--- /dev/null
+++ b/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch
@@ -0,0 +1,740 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Nassim Jahnke <[email protected]>
+Date: Sun, 30 Oct 2022 23:47:26 +0100
+Subject: [PATCH] Remap reflection calls in plugins using internals
+
+Co-authored-by: Jason Penilla <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 884ac16677ee3f52174c7bbf7b34896bcbf04bbc..49749e2bd8a4af96d2091fa1bccd876c2abb9e12 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -62,6 +62,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 b61935052154e76b1b8cb49868c96c52f34a41d1..a2fe12513b93ded71517955ef3e52c925f56f7d1 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 java.lang.reflect.Type;
+ import java.util.List;
+@@ -68,7 +69,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..0e734c07dbe82ba4c319a237f9e79b08b57b997f 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
+@@ -7,6 +7,6 @@ public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModi
+
+ @Override
+ public byte[] modify(PluginMeta configuration, byte[] bytecode) {
+- return bytecode;
++ return io.papermc.paper.pluginremap.reflect.ReflectionRemapper.processClass(bytecode);
+ }
+ }
+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..f871a329eb52da077f58d0ceaaabd3349f84cad0 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_REMAPPING
++ ? 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..1bf0fa1530b8e5f94d726d0313b7a00f675b500c 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_REMAPPING) {
++ 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..92bc8e4933ff13764fa2ac7f3729216332e202c9
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java
+@@ -0,0 +1,211 @@
++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.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 {
++ return ReflectionRemapper.processClass(bytes);
++ } 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..a3045afbc0cc057e99189b909367b21cf6a9e03f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java
+@@ -0,0 +1,66 @@
++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.ClassReader;
++import org.objectweb.asm.ClassVisitor;
++import org.objectweb.asm.ClassWriter;
++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() || MappingEnvironment.DISABLE_PLUGIN_REMAPPING) {
++ return parent;
++ }
++ return VISITOR_FACTORY.createVisitor(parent);
++ }
++
++ public static byte[] processClass(final byte[] bytes) {
++ if (MappingEnvironment.DISABLE_PLUGIN_REMAPPING) {
++ return bytes;
++ }
++ final ClassReader classReader = new ClassReader(bytes);
++ final ClassWriter classWriter = new ClassWriter(classReader, 0);
++ classReader.accept(ReflectionRemapper.visitor(classWriter), 0);
++ return classWriter.toByteArray();
++ }
++
++ 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..4477944f632a6b3936960ee80f9d898d3b7eed19 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_REMAPPING = Boolean.getBoolean("paper.disablePluginRemapping");
++ public static final String LEGACY_CB_VERSION = "v1_21_R2";
+ 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 7d2d5c4ee244e7118a4c38628e2e69c79b11b98d..371d31266a532e59c49dbb106e354296b119fa5e 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
+@@ -131,36 +131,26 @@ public class Commodore {
+ }
+
+ // 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
+
+@@ -245,6 +235,7 @@ public class Commodore {
+ visitor = new LimitedClassRemapper(cw, new SimpleRemapper(Commodore.ENUM_RENAMES));
+ }
+
++ visitor = io.papermc.paper.pluginremap.reflect.ReflectionRemapper.visitor(visitor); // Paper
+ cr.accept(new ClassRemapper(new ClassVisitor(Opcodes.ASM9, visitor) {
+ final Set<RerouteMethodData> rerouteMethodData = new HashSet<>();
+ String className;
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index 8e747d48fb76c3f8bacb28f270754d5caeca4445..07316f0043639c608dddadd1b3af871f4c3129d0 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -75,6 +75,7 @@ import org.bukkit.potion.PotionType;
+ @SuppressWarnings("deprecation")
+ public final class CraftMagicNumbers implements UnsafeValues {
+ public static final CraftMagicNumbers INSTANCE = new CraftMagicNumbers();
++ public static final boolean DISABLE_OLD_API_SUPPORT = Boolean.getBoolean("paper.disableOldApiSupport"); // Paper
+
+ private final Commodore commodore = new Commodore();
+
+@@ -347,7 +348,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 (!DISABLE_OLD_API_SUPPORT && toCheck.isOlderThan(ApiVersion.FLATTENING)) { // Paper
+ CraftLegacy.init();
+ }
+
+@@ -362,6 +363,12 @@ public final class CraftMagicNumbers implements UnsafeValues {
+
+ @Override
+ public byte[] processClass(PluginDescriptionFile pdf, String path, byte[] clazz) {
++ // Paper start
++ if (DISABLE_OLD_API_SUPPORT) {
++ // Make sure we still go through our reflection rewriting if needed
++ return io.papermc.paper.pluginremap.reflect.ReflectionRemapper.processClass(clazz);
++ }
++ // Paper end
+ try {
+ clazz = this.commodore.convert(clazz, pdf.getName(), ApiVersion.getOrCreateVersion(pdf.getAPIVersion()), ((CraftServer) Bukkit.getServer()).activeCompatibilities);
+ } catch (Exception ex) {