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 1053799ea1f654e1885c46d34a145b9d9fd34a21..13d392b6f2715f1b905d8b93a7cd8df3ce62f8e9 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") { + 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 mappings; + private final Map, Map, IntList>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(64, 0.75f, true) { + @Override + protected boolean removeEldestEntry(final Map.Entry, Map, IntList>> eldest) { + return this.size() > 63; + } + }); + + StacktraceDeobfuscator() { + this.mappings = loadMappingsIfPresent(); + } + + private static @Nullable Map 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.builder(); + + for (final ClassDef classDef : tree.getClasses()) { + final String obfClassName = classDef.getName(SPIGOT_NAMESPACE).replace('/', '.'); + final var methodMappings = ImmutableMap., 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 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, IntList> buildLineMap(final @NotNull Class key) { + final Map, 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 determineMethodForLine(final @NotNull Class clazz, final int lineNumber) { + final Map, IntList> lineMap = this.lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap); + for (final var entry : lineMap.entrySet()) { + final Pair 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, 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/CrashReportCategory.java b/src/main/java/net/minecraft/CrashReportCategory.java index 3941e14d1c3e6e688e28904948039c8b2200de5f..a4fda4a3bae9ce600e778b44cd3ef432a8b65667 100644 --- a/src/main/java/net/minecraft/CrashReportCategory.java +++ b/src/main/java/net/minecraft/CrashReportCategory.java @@ -104,6 +104,7 @@ public class CrashReportCategory { } else { this.stackTrace = new StackTraceElement[stackTraceElements.length - 3 - ignoredCallCount]; System.arraycopy(stackTraceElements, 3 + ignoredCallCount, this.stackTrace, 0, this.stackTrace.length); + this.stackTrace = io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(this.stackTrace); // Paper return this.stackTrace.length; } } 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 @@ + + + + + + - - - +