This is an automated email from the ASF dual-hosted git repository.

ramanathan1504 pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/2.x by this push:
     new 019a92865a Fix stack trace rendering for exceptions with identity 
malfunction (#4133)
019a92865a is described below

commit 019a92865a9bcfcceb2e56a21ae9356eedc736eb
Author: Ramanathan <[email protected]>
AuthorDate: Thu Jun 11 17:45:03 2026 +0530

    Fix stack trace rendering for exceptions with identity malfunction (#4133)
    
    * Fix circular reference detection for exceptions with colliding 
equals/hashCode implementations
    
    * Add changelog entry for circular reference detection fix with colliding 
equals/hashCode exceptions
    
    * Add changelog entry for circular reference detection fix with colliding 
equals/hashCode exceptions
    
    * Add test for ThrowableProxy serialization with colliding equals/hashCode 
implementations
    
    * Refactor ThrowableProxy serialization test to use modern Java I/O classes
    
    * Use `TestFriendlyException` to exercise the malfunction
    
    * Update changelog
    
    * Improve comments
    
    * Remove redundant change
    
    ---------
    
    Co-authored-by: Volkan Yazıcı <[email protected]>
---
 .../src/test/java/foo/TestFriendlyException.java   | 76 ++++++++++++++++++++--
 .../logging/log4j/core/impl/ThrowableProxy.java    | 14 ++--
 .../core/pattern/ThrowableStackTraceRenderer.java  | 16 +++--
 ...nce-detection-for-exceptions-with-colliding.xml | 13 ++++
 4 files changed, 104 insertions(+), 15 deletions(-)

diff --git a/log4j-core-test/src/test/java/foo/TestFriendlyException.java 
b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
index 7c791dc20d..c024db10ae 100644
--- a/log4j-core-test/src/test/java/foo/TestFriendlyException.java
+++ b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
@@ -20,6 +20,12 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 import java.net.Socket;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Set;
 import java.util.stream.Stream;
 import org.apache.logging.log4j.util.Constants;
 
@@ -33,6 +39,7 @@ import org.apache.logging.log4j.util.Constants;
  * <li>Suppressed exceptions</li>
  * <li>Clutter-free stack trace (i.e., elements from JUnit, JDK, etc.)</li>
  * <li>Stack trace elements from named modules<sup>3</sup></li>
+ * <li>Exceptions with malfunctioning (e.g., colliding) {@link 
Object#equals(Object) equals()} and {@link Object#hashCode() hashCode()} 
implementations in the causal chain</li>
  * </ul>
  * <p>
  * <sup>1</sup> Helps with observing stack trace manipulation effects of Log4j.
@@ -80,20 +87,63 @@ public final class TestFriendlyException extends 
RuntimeException {
         "java.lang", "jdk.internal", "org.junit", "sun.reflect"
     };
 
-    public static final TestFriendlyException INSTANCE = create("r", 0, 2, new 
boolean[] {false}, new boolean[] {true});
+    public static final TestFriendlyException INSTANCE =
+            create("r", 0, 2, new boolean[] {false}, new boolean[] {true}, new 
int[] {5});
+
+    static {
+        ensureIdentityMalfunctionAtDifferentDepths();
+    }
+
+    /**
+     * Ensure we have identity malfunctioning exceptions that have different 
stack trace lengths.
+     *
+     * @see <a 
href="https://github.com/apache/logging-log4j2/issues/3933";>#3933</a>
+     */
+    private static void ensureIdentityMalfunctionAtDifferentDepths() {
+        final Set<Throwable> visitedExceptions = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        final Set<Integer> identityMalfunctioningExceptionStackTraceDepths = 
new HashSet<>();
+        final Queue<TestFriendlyException> exceptions = new LinkedList<>();
+        exceptions.add(INSTANCE);
+        while (!exceptions.isEmpty()) {
+
+            // Process the exception
+            final TestFriendlyException exception = exceptions.remove();
+            if (!visitedExceptions.add(exception) || 
!exception.identityMalfunctioning) {
+                continue;
+            }
+            
identityMalfunctioningExceptionStackTraceDepths.add(exception.getStackTrace().length);
+
+            // Enqueue the cause
+            final TestFriendlyException cause = (TestFriendlyException) 
exception.getCause();
+            if (cause != null) {
+                exceptions.add(cause);
+            }
+
+            // Enqueue the suppressed
+            for (final Throwable suppressed : exception.getSuppressed()) {
+                exceptions.add((TestFriendlyException) suppressed);
+            }
+        }
+        assertThat(identityMalfunctioningExceptionStackTraceDepths)
+                .describedAs("# of visited exceptions = %s", 
visitedExceptions.size())
+                .hasSizeGreaterThan(1);
+    }
 
     private static TestFriendlyException create(
             final String name,
             final int depth,
             final int maxDepth,
             final boolean[] circular,
-            final boolean[] namedModuleAllowed) {
-        final TestFriendlyException error = new TestFriendlyException(name, 
namedModuleAllowed);
+            final boolean[] namedModuleAllowed,
+            final int[] maxIdentityMalfunctionCount) {
+        final TestFriendlyException error =
+                new TestFriendlyException(name, namedModuleAllowed, 
maxIdentityMalfunctionCount);
         if (depth < maxDepth) {
-            final TestFriendlyException cause = create(name + "_c", depth + 1, 
maxDepth, circular, namedModuleAllowed);
+            final TestFriendlyException cause =
+                    create(name + "_c", depth + 1, maxDepth, circular, 
namedModuleAllowed, maxIdentityMalfunctionCount);
             error.initCause(cause);
             final TestFriendlyException suppressed =
-                    create(name + "_s", depth + 1, maxDepth, circular, 
namedModuleAllowed);
+                    create(name + "_s", depth + 1, maxDepth, circular, 
namedModuleAllowed, maxIdentityMalfunctionCount);
             error.addSuppressed(suppressed);
             final boolean circularAllowed = depth + 1 == maxDepth && 
!circular[0];
             if (circularAllowed) {
@@ -105,8 +155,12 @@ public final class TestFriendlyException extends 
RuntimeException {
         return error;
     }
 
-    private TestFriendlyException(final String message, final boolean[] 
namedModuleAllowed) {
+    private final boolean identityMalfunctioning;
+
+    private TestFriendlyException(
+            final String message, final boolean[] namedModuleAllowed, final 
int[] maxIdentityMalfunctionCount) {
         super(message);
+        this.identityMalfunctioning = --maxIdentityMalfunctionCount[0] > 0;
         removeExcludedStackTraceElements(namedModuleAllowed);
     }
 
@@ -171,4 +225,14 @@ public final class TestFriendlyException extends 
RuntimeException {
     public String getLocalizedMessage() {
         return getMessage() + " [localized]";
     }
+
+    @Override
+    public int hashCode() {
+        return identityMalfunctioning ? 0 : super.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return identityMalfunctioning || super.equals(obj);
+    }
 }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java
index 61d292dc48..d051a4fc20 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java
@@ -18,9 +18,10 @@ package org.apache.logging.log4j.core.impl;
 
 import java.io.Serializable;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Deque;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -113,11 +114,16 @@ public class ThrowableProxy implements Serializable {
         this.extendedStackTrace =
                 ThrowableProxyHelper.toExtendedStackTrace(this, stack, map, 
null, throwable.getStackTrace());
         final Throwable throwableCause = throwable.getCause();
-        final Set<Throwable> causeVisited = new HashSet<>(1);
+        // `IdentityHashMap` is needed for exceptions with identity 
malfunction.
+        // Consider `equals()` and `hashCode()` implementations causing 
collisions.
+        final Set<Throwable> causeVisited = Collections.newSetFromMap(new 
IdentityHashMap<>(1));
+        final Set<Throwable> suppressedVisited =
+                visited == null ? Collections.newSetFromMap(new 
IdentityHashMap<>()) : visited;
+
         this.causeProxy = throwableCause == null
                 ? null
-                : new ThrowableProxy(throwable, stack, map, throwableCause, 
visited, causeVisited);
-        this.suppressedProxies = 
ThrowableProxyHelper.toSuppressedProxies(throwable, visited);
+                : new ThrowableProxy(throwable, stack, map, throwableCause, 
suppressedVisited, causeVisited);
+        this.suppressedProxies = 
ThrowableProxyHelper.toSuppressedProxies(throwable, suppressedVisited);
     }
 
     /**
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
index 4d21021321..cafb2a2a93 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
@@ -16,8 +16,8 @@
  */
 package org.apache.logging.log4j.core.pattern;
 
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -53,7 +53,10 @@ class ThrowableStackTraceRenderer<C extends 
ThrowableStackTraceRenderer.Context>
         if (maxLineCount > 0) {
             try {
                 C context = createContext(throwable);
-                renderThrowable(buffer, throwable, context, new HashSet<>(), 
lineSeparator);
+                // `IdentityHashMap` is needed for exceptions with identity 
malfunction.
+                // Consider `equals()` and `hashCode()` implementations 
causing collisions.
+                final Set<Throwable> visitedThrowables = 
Collections.newSetFromMap(new IdentityHashMap<>());
+                renderThrowable(buffer, throwable, context, visitedThrowables, 
lineSeparator);
             } catch (final Exception error) {
                 if (error != MAX_LINE_COUNT_EXCEEDED) {
                     throw error;
@@ -292,8 +295,11 @@ class ThrowableStackTraceRenderer<C extends 
ThrowableStackTraceRenderer.Context>
             }
 
             static Map<Throwable, Metadata> ofThrowable(final Throwable 
throwable) {
-                final Map<Throwable, Metadata> metadataByThrowable = new 
HashMap<>();
-                populateMetadata(metadataByThrowable, new HashSet<>(), null, 
throwable);
+                // `IdentityHashMap` is needed for exceptions with identity 
malfunction.
+                // Consider `equals()` and `hashCode()` implementations 
causing collisions.
+                final Map<Throwable, Metadata> metadataByThrowable = new 
IdentityHashMap<>();
+                final Set<Throwable> visitedThrowables = 
Collections.newSetFromMap(new IdentityHashMap<>());
+                populateMetadata(metadataByThrowable, visitedThrowables, null, 
throwable);
                 return metadataByThrowable;
             }
 
diff --git 
a/src/changelog/.2.x.x/fix-circular-reference-detection-for-exceptions-with-colliding.xml
 
b/src/changelog/.2.x.x/fix-circular-reference-detection-for-exceptions-with-colliding.xml
new file mode 100644
index 0000000000..1633dcdb8a
--- /dev/null
+++ 
b/src/changelog/.2.x.x/fix-circular-reference-detection-for-exceptions-with-colliding.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="changed">
+  <issue id="3933" 
link="https://github.com/apache/logging-log4j2/issues/3933"/>
+  <issue id="4133" link="https://github.com/apache/logging-log4j2/pull/4133"/>
+  <description format="asciidoc">
+    Fix stack trace rendering for exceptions with identity malfunction (e.g., 
colliding `equals()` and/or `hashCode()` implementations)
+  </description>
+</entry>

Reply via email to