Remap reflection calls in plugins using internals

Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
This commit is contained in:
Nassim Jahnke
2022-10-30 23:47:26 +01:00
parent 4cd66b0305
commit fae673b95c
12 changed files with 518 additions and 28 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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