Paper/patches/server/0716-Deobfuscate-stacktraces-in-log-messages-crash-report.patch
Kyle Wood e72fa4144f
Update task dependency for includeMappings so the new task isn't skipped
The new task fixJarForReobf was added after shadowJar, but since
reobfJar's input is changed in this patch, that new task needs to be
referenced instead of shadowJar.
2021-06-26 22:26:17 -05:00

471 lines
20 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sun, 20 Jun 2021 18:19:09 -0700
Subject: [PATCH] Deobfuscate stacktraces in log messages, crash reports, and
etc.
diff --git a/build.gradle.kts b/build.gradle.kts
index 45b08a01495dc42c36f26a6b8e833c9ae81079e8..7f2634a90b63f39a61340ee23e2b42f712d48e48 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,8 +1,12 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
+import io.papermc.paperweight.tasks.BaseTask
import io.papermc.paperweight.util.Git
+import io.papermc.paperweight.util.defaultOutput
+import io.papermc.paperweight.util.openZip
import io.papermc.paperweight.util.path
import shadow.org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE
+import java.nio.file.Files
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -15,6 +19,14 @@ plugins {
repositories {
maven("https://libraries.minecraft.net/")
+ // Paper start
+ maven("https://maven.quiltmc.org/repository/release/") {
+ mavenContent {
+ releasesOnly()
+ includeModule("org.quiltmc", "tiny-mappings-parser")
+ }
+ }
+ // Paper end
}
dependencies {
@@ -52,6 +64,8 @@ dependencies {
implementation("com.github.oshi:oshi-core:5.7.5") // Paper - fix startup delay and warning
+ implementation("org.quiltmc:tiny-mappings-parser:0.3.0") // Paper - needed to read mappings for stacktrace deobfuscation
+
testImplementation("io.github.classgraph:classgraph:4.8.47") // Paper - mob goal test
testImplementation("junit:junit:4.13.1")
testImplementation("org.hamcrest:hamcrest-library:1.3")
@@ -116,6 +130,44 @@ tasks.shadowJar {
transform(ModifiedLog4j2PluginsCacheFileTransformer::class.java)
}
+// Paper start - include reobf mappings in jar for stacktrace deobfuscation
+abstract class IncludeMappings : BaseTask() {
+ @get:InputFile
+ abstract val inputJar: RegularFileProperty
+
+ @get:InputFile
+ abstract val mappings: RegularFileProperty
+
+ @get:OutputFile
+ abstract val outputJar: RegularFileProperty
+
+ override fun init() {
+ outputJar.convention(defaultOutput())
+ }
+
+ @TaskAction
+ private fun addMappings() {
+ outputJar.get().asFile.parentFile.mkdirs()
+ inputJar.get().asFile.copyTo(outputJar.get().asFile, overwrite = true)
+ outputJar.get().path.openZip().use { fs ->
+ val dir = fs.getPath("META-INF/mappings/")
+ Files.createDirectories(dir)
+ val target = dir.resolve("reobf.tiny")
+ Files.copy(mappings.path, target)
+ }
+ }
+}
+
+val includeMappings = tasks.register<IncludeMappings>("includeMappings") {
+ inputJar.set(tasks.fixJarForReobf.flatMap { it.outputJar })
+ mappings.set(tasks.reobfJar.flatMap { it.mappingsFile })
+}
+
+tasks.reobfJar {
+ inputJar.set(includeMappings.flatMap { it.outputJar })
+}
+// Paper end - include reobf mappings in jar for stacktrace deobfuscation
+
tasks.test {
exclude("org/bukkit/craftbukkit/inventory/ItemStack*Test.class")
}
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
index 7acf077bc131af718c7548cc29deef558c04e463..73652e403eaa419fb5b4b54bd506f246c0aa4e99 100644
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -487,6 +487,11 @@ public class PaperConfig {
enableBrigadierConsoleCompletions = getBoolean("settings.console.enable-brigadier-completions", enableBrigadierConsoleCompletions);
}
+ public static boolean deobfuscateStacktraces = true;
+ private static void loggerSettings() {
+ deobfuscateStacktraces = getBoolean("settings.loggers.deobfuscate-stacktraces", deobfuscateStacktraces);
+ }
+
public static int itemValidationDisplayNameLength = 8192;
public static int itemValidationLocNameLength = 8192;
public static int itemValidationLoreLineLength = 8192;
diff --git a/src/main/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java b/src/main/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..d019802a36dbaca4bf299a55d28381e43d6b976f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java
@@ -0,0 +1,31 @@
+package io.papermc.paper.logging;
+
+import io.papermc.paper.util.StacktraceDeobfuscator;
+import org.apache.logging.log4j.core.Core;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.jetbrains.annotations.NotNull;
+
+@Plugin(
+ name = "StacktraceDeobfuscatingRewritePolicy",
+ category = Core.CATEGORY_NAME,
+ elementType = "rewritePolicy",
+ printObject = true
+)
+public final class StacktraceDeobfuscatingRewritePolicy implements RewritePolicy {
+ @Override
+ public @NotNull LogEvent rewrite(final @NotNull LogEvent rewrite) {
+ final Throwable thrown = rewrite.getThrown();
+ if (thrown != null) {
+ StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(thrown);
+ }
+ return rewrite;
+ }
+
+ @PluginFactory
+ public static @NotNull StacktraceDeobfuscatingRewritePolicy createPolicy() {
+ return new StacktraceDeobfuscatingRewritePolicy();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
new file mode 100644
index 0000000000000000000000000000000000000000..281fd2dbc7f1281ba882020be2a3481fe8909685
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
@@ -0,0 +1,223 @@
+package io.papermc.paper.util;
+
+import com.destroystokyo.paper.PaperConfig;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableMap;
+import com.mojang.datafixers.util.Pair;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import net.fabricmc.mapping.tree.ClassDef;
+import net.fabricmc.mapping.tree.MethodDef;
+import net.fabricmc.mapping.tree.TinyMappingFactory;
+import net.fabricmc.mapping.tree.TinyTree;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public enum StacktraceDeobfuscator {
+ INSTANCE;
+
+ private static final String MOJANG_PLUS_YARN_NAMESPACE = "mojang+yarn";
+ private static final String SPIGOT_NAMESPACE = "spigot";
+
+ private final @Nullable Map<String, ClassMapping> mappings;
+ private final Map<Class<?>, Map<Pair<String, String>, IntList>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(64, 0.75f, true) {
+ @Override
+ protected boolean removeEldestEntry(final Map.Entry<Class<?>, Map<Pair<String, String>, IntList>> eldest) {
+ return this.size() > 63;
+ }
+ });
+
+ StacktraceDeobfuscator() {
+ this.mappings = loadMappingsIfPresent();
+ }
+
+ private static @Nullable Map<String, ClassMapping> loadMappingsIfPresent() {
+ try (final InputStream mappingsInputStream = StacktraceDeobfuscator.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) {
+ if (mappingsInputStream == null) {
+ return null;
+ }
+ final TinyTree tree = TinyMappingFactory.loadWithDetection(new BufferedReader(new InputStreamReader(mappingsInputStream, Charsets.UTF_8)));
+ final var builder = ImmutableMap.<String, ClassMapping>builder();
+
+ for (final ClassDef classDef : tree.getClasses()) {
+ final String obfClassName = classDef.getName(SPIGOT_NAMESPACE).replace('/', '.');
+ final var methodMappings = ImmutableMap.<Pair<String, String>, MethodMapping>builder();
+
+ for (final MethodDef methodDef : classDef.getMethods()) {
+ final MethodMapping method = new MethodMapping(
+ methodDef.getName(SPIGOT_NAMESPACE),
+ methodDef.getName(MOJANG_PLUS_YARN_NAMESPACE),
+ methodDef.getDescriptor(SPIGOT_NAMESPACE)
+ );
+ methodMappings.put(
+ new Pair<>(method.obfName(), method.descriptor()),
+ method
+ );
+ }
+
+ final ClassMapping map = new ClassMapping(
+ obfClassName,
+ classDef.getName(MOJANG_PLUS_YARN_NAMESPACE).replace('/', '.'),
+ methodMappings.build()
+ );
+ builder.put(map.obfName(), map);
+ }
+
+ return builder.build();
+ } catch (final IOException ex) {
+ System.err.println("Failed to load mappings for stacktrace deobfuscation.");
+ ex.printStackTrace();
+ return null;
+ }
+ }
+
+ public void deobfuscateThrowable(final Throwable throwable) {
+ if (!PaperConfig.deobfuscateStacktraces) {
+ return;
+ }
+
+ throwable.setStackTrace(this.deobfuscateStacktrace(throwable.getStackTrace()));
+ final Throwable cause = throwable.getCause();
+ if (cause != null) {
+ this.deobfuscateThrowable(cause);
+ }
+ for (final Throwable suppressed : throwable.getSuppressed()) {
+ this.deobfuscateThrowable(suppressed);
+ }
+ }
+
+ public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) {
+ if (!PaperConfig.deobfuscateStacktraces) {
+ return traceElements;
+ }
+
+ if (this.mappings == null || traceElements.length == 0) {
+ return traceElements;
+ }
+ final StackTraceElement[] result = new StackTraceElement[traceElements.length];
+ for (int i = 0; i < traceElements.length; i++) {
+ final StackTraceElement element = traceElements[i];
+
+ final String className = element.getClassName();
+ final String methodName = element.getMethodName();
+
+ final ClassMapping classMapping = this.mappings.get(className);
+ if (classMapping == null) {
+ result[i] = element;
+ continue;
+ }
+
+ final Pair<String, String> nameDescriptorPair;
+ try {
+ final Class<?> clazz = Class.forName(className);
+ nameDescriptorPair = this.determineMethodForLine(clazz, element.getLineNumber());
+ } catch (final ReflectiveOperationException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ final MethodMapping methodMapping = classMapping.methodMappings().get(nameDescriptorPair);
+
+ result[i] = new StackTraceElement(
+ element.getClassLoaderName(),
+ element.getModuleName(),
+ element.getModuleVersion(),
+ classMapping.mojangName(),
+ methodMapping != null ? methodMapping.mojangName() : methodName,
+ this.mappedFileName(classMapping.mojangName()),
+ element.getLineNumber()
+ );
+ }
+ return result;
+ }
+
+ private static @NotNull Map<Pair<String, String>, IntList> buildLineMap(final @NotNull Class<?> key) {
+ final Map<Pair<String, String>, IntList> lineMap = new HashMap<>();
+ final class LineCollectingMethodVisitor extends MethodVisitor {
+ private final IntList lines = new IntArrayList();
+ private final String name;
+ private final String descriptor;
+
+ LineCollectingMethodVisitor(String name, String descriptor) {
+ super(Opcodes.ASM9);
+ this.name = name;
+ this.descriptor = descriptor;
+ }
+
+ @Override
+ public void visitLineNumber(int line, Label start) {
+ super.visitLineNumber(line, start);
+ this.lines.add(line);
+ }
+
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ lineMap.put(new Pair<>(this.name, this.descriptor), this.lines);
+ }
+ }
+ final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ return new LineCollectingMethodVisitor(name, descriptor);
+ }
+ };
+ try {
+ final ClassReader reader = new ClassReader(key.getName());
+ reader.accept(classVisitor, 0);
+ } catch (final IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ return lineMap;
+ }
+
+ private @Nullable Pair<String, String> determineMethodForLine(final @NotNull Class<?> clazz, final int lineNumber) {
+ final Map<Pair<String, String>, IntList> lineMap = this.lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap);
+ for (final var entry : lineMap.entrySet()) {
+ final Pair<String, String> pair = entry.getKey();
+ final IntList lines = entry.getValue();
+ for (int i = 0, linesSize = lines.size(); i < linesSize; i++) {
+ final int num = lines.getInt(i);
+ if (num == lineNumber) {
+ return pair;
+ }
+ }
+ }
+ return null;
+ }
+
+ private @NotNull String mappedFileName(final @NotNull String fullClassName) {
+ final int dot = fullClassName.lastIndexOf('.');
+ final String className = dot == -1
+ ? fullClassName
+ : fullClassName.substring(dot + 1);
+ final String rootClassName = className.split("\\$")[0];
+ return rootClassName + ".java";
+ }
+
+ private record ClassMapping(
+ String obfName,
+ String mojangName,
+ Map<Pair<String, String>, MethodMapping> methodMappings
+ ) {
+ }
+
+ private record MethodMapping(
+ String obfName,
+ String mojangName,
+ String descriptor
+ ) {
+ }
+}
diff --git a/src/main/java/io/papermc/paper/util/TraceUtil.java b/src/main/java/io/papermc/paper/util/TraceUtil.java
index 2d5494d2813b773e60ddba6790b750a9a08f21f8..7695bf44503f161523ea612ef8a884ae574a2e21 100644
--- a/src/main/java/io/papermc/paper/util/TraceUtil.java
+++ b/src/main/java/io/papermc/paper/util/TraceUtil.java
@@ -6,13 +6,15 @@ public final class TraceUtil {
public static void dumpTraceForThread(Thread thread, String reason) {
Bukkit.getLogger().warning(thread.getName() + ": " + reason);
- StackTraceElement[] trace = thread.getStackTrace();
+ StackTraceElement[] trace = StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace());
for (StackTraceElement traceElement : trace) {
Bukkit.getLogger().warning("\tat " + traceElement);
}
}
public static void dumpTraceForThread(String reason) {
- new Throwable(reason).printStackTrace();
+ final Throwable throwable = new Throwable(reason);
+ StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(throwable);
+ throwable.printStackTrace();
}
}
diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java
index 09f56e49383d3f5413ad4c28f3a7664e4d9570bd..afb4fd59bfc8cf71d3a7fdc1cdf1b4c565146952 100644
--- a/src/main/java/net/minecraft/CrashReport.java
+++ b/src/main/java/net/minecraft/CrashReport.java
@@ -29,6 +29,7 @@ public class CrashReport {
private final SystemReport systemReport = new SystemReport();
public CrashReport(String message, Throwable cause) {
+ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(cause); // Paper
this.title = message;
this.exception = cause;
this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index eeefbb86cb88bd1b132bb6e22b4a4572cfb5e22c..6a1fd7a983724d9e16e8aef06052108ba7ed46f1 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -220,6 +220,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
com.destroystokyo.paper.PaperConfig.registerCommands();
com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // load version history now
io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider
+ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.getClass(); // load mappings for stacktrace deobf
// Paper end
this.setPvpAllowed(dedicatedserverproperties.pvp);
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
index 4d271cae88c16ed2419f896c728fdff612540500..dcfbe77bdb25d9c58ffb7b75c48bdb580bc0de47 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
@@ -106,7 +106,7 @@ public class WatchdogThread extends Thread
log.log( Level.SEVERE, "During the run of the server, a plugin set an excessive velocity on an entity" );
log.log( Level.SEVERE, "This may be the cause of the issue, or it may be entirely unrelated" );
log.log( Level.SEVERE, org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getMessage());
- for ( StackTraceElement stack : org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getStackTrace() )
+ for ( StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getStackTrace()) ) // Paper
{
log.log( Level.SEVERE, "\t\t" + stack );
}
@@ -194,7 +194,7 @@ public class WatchdogThread extends Thread
}
log.log( Level.SEVERE, "\tStack:" );
//
- for ( StackTraceElement stack : thread.getStackTrace() )
+ for ( StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace()) ) // Paper
{
log.log( Level.SEVERE, "\t\t" + stack );
}
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
index 67da1aa7a21622fb231d19dede3775a282a4a12e..f91d0df569b2f1d430ea5eee5f53779902a4f32c 100644
--- a/src/main/resources/log4j2.xml
+++ b/src/main/resources/log4j2.xml
@@ -29,15 +29,19 @@
</Policies>
<DefaultRolloverStrategy max="1000"/>
</RollingRandomAccessFile>
+ <Rewrite name="rewrite">
+ <StacktraceDeobfuscatingRewritePolicy />
+ <AppenderRef ref="File"/>
+ <AppenderRef ref="TerminalConsole" level="info"/>
+ <AppenderRef ref="ServerGuiConsole" level="info"/>
+ </Rewrite>
</Appenders>
<Loggers>
<Root level="info">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
</filters>
- <AppenderRef ref="File"/>
- <AppenderRef ref="TerminalConsole" level="info"/>
- <AppenderRef ref="ServerGuiConsole" level="info"/>
+ <AppenderRef ref="rewrite"/>
</Root>
</Loggers>
</Configuration>