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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -0,0 +0,0 @@ dependencies {
     testImplementation("org.mockito:mockito-core:5.11.0")
     testImplementation("org.ow2.asm:asm-tree:9.7")
     testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
+    implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling
 }
 
 paperweight {
diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java b/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java
@@ -0,0 +0,0 @@
+package io.papermc.paper.logging;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+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.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+@Plugin(
+    name = "StacktraceDeobfuscatingRewritePolicy",
+    category = Core.CATEGORY_NAME,
+    elementType = "rewritePolicy",
+    printObject = true
+)
+public final class StacktraceDeobfuscatingRewritePolicy implements RewritePolicy {
+    private static final MethodHandle DEOBFUSCATE_THROWABLE;
+
+    static {
+        try {
+            final Class<?> cls = Class.forName("io.papermc.paper.util.StacktraceDeobfuscator");
+            final MethodHandles.Lookup lookup = MethodHandles.lookup();
+            final VarHandle instanceHandle = lookup.findStaticVarHandle(cls, "INSTANCE", cls);
+            final Object deobfuscator = instanceHandle.get();
+            DEOBFUSCATE_THROWABLE = lookup
+                .unreflect(cls.getDeclaredMethod("deobfuscateThrowable", Throwable.class))
+                .bindTo(deobfuscator);
+        } catch (final ReflectiveOperationException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private StacktraceDeobfuscatingRewritePolicy() {
+    }
+
+    @Override
+    public @NonNull LogEvent rewrite(final @NonNull LogEvent rewrite) {
+        final Throwable thrown = rewrite.getThrown();
+        if (thrown != null) {
+            deobfuscateThrowable(thrown);
+            return new Log4jLogEvent.Builder(rewrite)
+                .setThrownProxy(null)
+                .build();
+        }
+        return rewrite;
+    }
+
+    private static void deobfuscateThrowable(final Throwable thrown) {
+        try {
+            DEOBFUSCATE_THROWABLE.invoke(thrown);
+        } catch (final Error e) {
+            throw e;
+        } catch (final Throwable e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @PluginFactory
+    public static @NonNull StacktraceDeobfuscatingRewritePolicy createPolicy() {
+        return new StacktraceDeobfuscatingRewritePolicy();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/util/ObfHelper.java b/src/main/java/io/papermc/paper/util/ObfHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/ObfHelper.java
@@ -0,0 +0,0 @@
+package io.papermc.paper.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import net.neoforged.srgutils.IMappingFile;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
+public enum ObfHelper {
+    INSTANCE;
+
+    private final @Nullable Map<String, ClassMapping> mappingsByObfName;
+    private final @Nullable Map<String, ClassMapping> mappingsByMojangName;
+
+    ObfHelper() {
+        final @Nullable Set<ClassMapping> maps = loadMappingsIfPresent();
+        if (maps != null) {
+            this.mappingsByObfName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::obfName, map -> map));
+            this.mappingsByMojangName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::mojangName, map -> map));
+        } else {
+            this.mappingsByObfName = null;
+            this.mappingsByMojangName = null;
+        }
+    }
+
+    public @Nullable Map<String, ClassMapping> mappingsByObfName() {
+        return this.mappingsByObfName;
+    }
+
+    public @Nullable Map<String, ClassMapping> mappingsByMojangName() {
+        return this.mappingsByMojangName;
+    }
+
+    /**
+     * Attempts to get the obf name for a given class by its Mojang name. Will
+     * return the input string if mappings are not present.
+     *
+     * @param fullyQualifiedMojangName fully qualified class name (dotted)
+     * @return mapped or original fully qualified (dotted) class name
+     */
+    public String reobfClassName(final String fullyQualifiedMojangName) {
+        if (this.mappingsByMojangName == null) {
+            return fullyQualifiedMojangName;
+        }
+
+        final ClassMapping map = this.mappingsByMojangName.get(fullyQualifiedMojangName);
+        if (map == null) {
+            return fullyQualifiedMojangName;
+        }
+
+        return map.obfName();
+    }
+
+    /**
+     * Attempts to get the Mojang name for a given class by its obf name. Will
+     * return the input string if mappings are not present.
+     *
+     * @param fullyQualifiedObfName fully qualified class name (dotted)
+     * @return mapped or original fully qualified (dotted) class name
+     */
+    public String deobfClassName(final String fullyQualifiedObfName) {
+        if (this.mappingsByObfName == null) {
+            return fullyQualifiedObfName;
+        }
+
+        final ClassMapping map = this.mappingsByObfName.get(fullyQualifiedObfName);
+        if (map == null) {
+            return fullyQualifiedObfName;
+        }
+
+        return map.mojangName();
+    }
+
+    private static @Nullable Set<ClassMapping> loadMappingsIfPresent() {
+        try (final @Nullable InputStream mappingsInputStream = ObfHelper.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) {
+            if (mappingsInputStream == null) {
+                return null;
+            }
+            final IMappingFile mappings = IMappingFile.load(mappingsInputStream); // Mappings are mojang->spigot
+            final Set<ClassMapping> classes = new HashSet<>();
+
+            final StringPool pool = new StringPool();
+            for (final IMappingFile.IClass cls : mappings.getClasses()) {
+                final Map<String, String> methods = new HashMap<>();
+                final Map<String, String> fields = new HashMap<>();
+                final Map<String, String> strippedMethods = new HashMap<>();
+
+                for (final IMappingFile.IMethod methodMapping : cls.getMethods()) {
+                    methods.put(
+                            pool.string(methodKey(
+                                    Objects.requireNonNull(methodMapping.getMapped()),
+                                    Objects.requireNonNull(methodMapping.getMappedDescriptor())
+                            )),
+                            pool.string(Objects.requireNonNull(methodMapping.getOriginal()))
+                    );
+
+                    strippedMethods.put(
+                            pool.string(pool.string(strippedMethodKey(
+                                    methodMapping.getMapped(),
+                                    methodMapping.getDescriptor()
+                            ))),
+                            pool.string(methodMapping.getOriginal())
+                    );
+                }
+                for (final IMappingFile.IField field : cls.getFields()) {
+                    fields.put(
+                            pool.string(field.getMapped()),
+                            pool.string(field.getOriginal())
+                    );
+                }
+
+                final ClassMapping map = new ClassMapping(
+                        Objects.requireNonNull(cls.getMapped()).replace('/', '.'),
+                        Objects.requireNonNull(cls.getOriginal()).replace('/', '.'),
+                        Map.copyOf(methods),
+                        Map.copyOf(fields),
+                        Map.copyOf(strippedMethods)
+                );
+                classes.add(map);
+            }
+
+            return Set.copyOf(classes);
+        } catch (final IOException ex) {
+            System.err.println("Failed to load mappings.");
+            ex.printStackTrace();
+            return null;
+        }
+    }
+
+    public static String strippedMethodKey(final String methodName, final String methodDescriptor) {
+        final String methodKey = methodKey(methodName, methodDescriptor);
+        final int returnDescriptorEnd = methodKey.indexOf(')');
+        return methodKey.substring(0, returnDescriptorEnd + 1);
+    }
+
+    public static String methodKey(final String methodName, final String methodDescriptor) {
+        return methodName + methodDescriptor;
+    }
+
+    public record ClassMapping(
+            String obfName,
+            String mojangName,
+            Map<String, String> methodsByObf,
+            Map<String, String> fieldsByObf,
+            // obf name with mapped desc to mapped name. return value is excluded from desc as reflection doesn't use it
+            Map<String, String> strippedMethods
+    ) {}
+}
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..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
@@ -0,0 +0,0 @@
+package io.papermc.paper.util;
+
+import io.papermc.paper.configuration.GlobalConfiguration;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+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.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@DefaultQualifier(NonNull.class)
+public enum StacktraceDeobfuscator {
+    INSTANCE;
+
+    private final Map<Class<?>, Int2ObjectMap<String>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(128, 0.75f, true) {
+        @Override
+        protected boolean removeEldestEntry(final Map.Entry<Class<?>, Int2ObjectMap<String>> eldest) {
+            return this.size() > 127;
+        }
+    });
+
+    public void deobfuscateThrowable(final Throwable throwable) {
+        if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
+            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 (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
+            return traceElements;
+        }
+
+        final @Nullable Map<String, ObfHelper.ClassMapping> mappings = ObfHelper.INSTANCE.mappingsByObfName();
+        if (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 ObfHelper.ClassMapping classMapping = mappings.get(className);
+            if (classMapping == null) {
+                result[i] = element;
+                continue;
+            }
+
+            final Class<?> clazz;
+            try {
+                clazz = Class.forName(className);
+            } catch (final ClassNotFoundException ex) {
+                throw new RuntimeException(ex);
+            }
+            final @Nullable String methodKey = this.determineMethodForLine(clazz, element.getLineNumber());
+            final @Nullable String mappedMethodName = methodKey == null ? null : classMapping.methodsByObf().get(methodKey);
+
+            result[i] = new StackTraceElement(
+                element.getClassLoaderName(),
+                element.getModuleName(),
+                element.getModuleVersion(),
+                classMapping.mojangName(),
+                mappedMethodName != null ? mappedMethodName : methodName,
+                sourceFileName(classMapping.mojangName()),
+                element.getLineNumber()
+            );
+        }
+        return result;
+    }
+
+    private @Nullable String determineMethodForLine(final Class<?> clazz, final int lineNumber) {
+        return this.lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap).get(lineNumber);
+    }
+
+    private static String sourceFileName(final 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 static Int2ObjectMap<String> buildLineMap(final Class<?> key) {
+        final StringPool pool = new StringPool();
+        final Int2ObjectMap<String> lineMap = new Int2ObjectOpenHashMap<>();
+        final class LineCollectingMethodVisitor extends MethodVisitor {
+            private final String name;
+            private final String descriptor;
+
+            LineCollectingMethodVisitor(final String name, final String descriptor) {
+                super(Opcodes.ASM9);
+                this.name = name;
+                this.descriptor = descriptor;
+            }
+
+            @Override
+            public void visitLineNumber(final int line, final Label start) {
+                lineMap.put(line, pool.string(ObfHelper.methodKey(this.name, this.descriptor)));
+            }
+        }
+        final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) {
+                return new LineCollectingMethodVisitor(name, descriptor);
+            }
+        };
+        try {
+            final @Nullable InputStream inputStream = StacktraceDeobfuscator.class.getClassLoader()
+                .getResourceAsStream(key.getName().replace('.', '/') + ".class");
+            if (inputStream == null) {
+                throw new IllegalStateException("Could not find class file: " + key.getName());
+            }
+            final byte[] classData;
+            try (inputStream) {
+                classData = inputStream.readAllBytes();
+            }
+            final ClassReader reader = new ClassReader(classData);
+            reader.accept(classVisitor, 0);
+        } catch (final IOException ex) {
+            throw new RuntimeException(ex);
+        }
+        return lineMap;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/util/StringPool.java b/src/main/java/io/papermc/paper/util/StringPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/StringPool.java
@@ -0,0 +0,0 @@
+package io.papermc.paper.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+/**
+ * De-duplicates {@link String} instances without using {@link String#intern()}.
+ *
+ * <p>Interning may not be desired as we may want to use the heap for our pool,
+ * so it can be garbage collected as normal, etc.</p>
+ *
+ * <p>Additionally, interning can be slow due to the potentially large size of the
+ * pool (as it is shared for the entire JVM), and because most JVMs implement
+ * it using JNI.</p>
+ */
+@DefaultQualifier(NonNull.class)
+public final class StringPool {
+    private final Map<String, String> pool;
+
+    public StringPool() {
+        this(new HashMap<>());
+    }
+
+    public StringPool(final Map<String, String> map) {
+        this.pool = map;
+    }
+
+    public String string(final String string) {
+        return this.pool.computeIfAbsent(string, Function.identity());
+    }
+}
diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/CrashReport.java
+++ b/src/main/java/net/minecraft/CrashReport.java
@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/CrashReportCategory.java
+++ b/src/main/java/net/minecraft/CrashReportCategory.java
@@ -0,0 +0,0 @@ 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/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/Connection.java
+++ b/src/main/java/net/minecraft/network/Connection.java
@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
         marker.add(Connection.PACKET_MARKER);
     });
     public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(() -> {
-        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).build());
+        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
     });
     public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(() -> {
-        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build());
+        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
     });
     public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(() -> {
-        return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).build());
+        return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
     });
     private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
     private final PacketFlow receiving;
@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
 
             }
         }
-        if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) throwable.printStackTrace(); // Spigot
+        if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper
     }
 
     protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet<?> packet) {
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -0,0 +0,0 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
         org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings"));
         org.spigotmc.SpigotConfig.registerCommands();
         // Spigot end
+        io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // Paper - load mappings for stacktrace deobf and etc.
         // Paper start - initialize global and world-defaults configuration
         this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess());
         this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
+++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
@@ -0,0 +0,0 @@ public class ServerConnectionListener {
 
     private static final Logger LOGGER = LogUtils.getLogger();
     public static final Supplier<NioEventLoopGroup> SERVER_EVENT_GROUP = Suppliers.memoize(() -> {
-        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).build());
+        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
     });
     public static final Supplier<EpollEventLoopGroup> SERVER_EPOLL_EVENT_GROUP = Suppliers.memoize(() -> {
-        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build());
+        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
     });
     final MinecraftServer server;
     public volatile boolean running;
diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java
+++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
@@ -0,0 +0,0 @@ public class OldUsersConverter {
                         try {
                             root = NbtIo.readCompressed(new java.io.FileInputStream(file5), NbtAccounter.unlimitedHeap());
                         } catch (Exception exception) {
-                            exception.printStackTrace();
+                            io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper
                         }
 
                         if (root != null) {
@@ -0,0 +0,0 @@ public class OldUsersConverter {
                             try {
                                 NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2));
                             } catch (Exception exception) {
-                                exception.printStackTrace();
+                                io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper
                             }
                        }
                         // CraftBukkit end
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/resources/log4j2.xml
+++ b/src/main/resources/log4j2.xml
@@ -0,0 +0,0 @@
             <DefaultRolloverStrategy max="1000"/>
         </RollingRandomAccessFile>
         <Async name="Async">
+            <AppenderRef ref="rewrite"/>
+        </Async>
+        <Rewrite name="rewrite">
+            <StacktraceDeobfuscatingRewritePolicy />
             <AppenderRef ref="File"/>
             <AppenderRef ref="TerminalConsole" level="info"/>
             <AppenderRef ref="ServerGuiConsole" level="info"/>
-        </Async>
+        </Rewrite>
     </Appenders>
     <Loggers>
         <Root level="info">
