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

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

commit 53e2b7690972f1943ef802252a137cb3d5ec82c1
Author: Ramanathan <[email protected]>
AuthorDate: Sun May 31 15:55:22 2026 +0530

    Fix circular reference detection for exceptions with colliding 
equals/hashCode implementations
---
 .../core/test/impl/ThrowableCollisionTest.java     | 95 ++++++++++++++++++++++
 .../logging/log4j/core/impl/ThrowableProxy.java    | 12 ++-
 .../core/pattern/ThrowableStackTraceRenderer.java  | 16 ++--
 3 files changed, 113 insertions(+), 10 deletions(-)

diff --git 
a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/impl/ThrowableCollisionTest.java
 
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/impl/ThrowableCollisionTest.java
new file mode 100644
index 0000000000..b14237732d
--- /dev/null
+++ 
b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/impl/ThrowableCollisionTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.test.impl;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Objects;
+import org.apache.logging.log4j.core.impl.ThrowableProxy;
+import org.junit.jupiter.api.Test;
+
+public class ThrowableCollisionTest {
+
+    static class CollidingException extends RuntimeException {
+        public CollidingException(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            return obj instanceof CollidingException
+                    && Objects.equals(getMessage(), ((CollidingException) 
obj).getMessage());
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(getMessage());
+        }
+    }
+
+    static class CyclicException extends RuntimeException {
+        private Throwable customCause;
+
+        public CyclicException(String message) {
+            super(message);
+        }
+
+        public void setCustomCause(Throwable cause) {
+            this.customCause = cause;
+        }
+
+        @Override
+        public Throwable getCause() {
+            return customCause;
+        }
+    }
+
+    @Test
+    public void testCollisionDoesNotTriggerCircularReference() {
+        Throwable inner = new CollidingException("collision", null);
+        Throwable outer = new CollidingException("collision", inner);
+
+        assertDoesNotThrow(() -> {
+            ThrowableProxy proxy = new ThrowableProxy(outer);
+            String trace = proxy.getExtendedStackTraceAsString();
+
+            assertFalse(
+                    trace.contains("CIRCULAR REFERENCE"),
+                    "Should not mark a non-cyclic colliding exception chain as 
circular!");
+        });
+    }
+
+    @Test
+    public void testTrueCircularReferenceIsStillHandledSafely() {
+        CyclicException ex1 = new CyclicException("Cycle Exception 1");
+        CyclicException ex2 = new CyclicException("Cycle Exception 2");
+
+        ex1.setCustomCause(ex2);
+        ex2.setCustomCause(ex1);
+
+        assertDoesNotThrow(() -> {
+            ThrowableProxy proxy = new ThrowableProxy(ex1);
+            String trace = proxy.getExtendedStackTraceAsString();
+
+            assertTrue(
+                    trace.contains("CIRCULAR REFERENCE"),
+                    "Should successfully detect and flag a genuine cyclic 
reference!");
+        });
+    }
+}
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..eb05610f5b 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,14 @@ 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);
+        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..c6ab8e22e0 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,8 @@ class ThrowableStackTraceRenderer<C extends 
ThrowableStackTraceRenderer.Context>
         if (maxLineCount > 0) {
             try {
                 C context = createContext(throwable);
-                renderThrowable(buffer, throwable, context, new HashSet<>(), 
lineSeparator);
+                renderThrowable(
+                        buffer, throwable, context, 
Collections.newSetFromMap(new IdentityHashMap<>()), lineSeparator);
             } catch (final Exception error) {
                 if (error != MAX_LINE_COUNT_EXCEEDED) {
                     throw error;
@@ -64,7 +65,9 @@ class ThrowableStackTraceRenderer<C extends 
ThrowableStackTraceRenderer.Context>
 
     @SuppressWarnings("unchecked")
     C createContext(final Throwable throwable) {
-        final Map<Throwable, Context.Metadata> metadataByThrowable = 
Context.Metadata.ofThrowable(throwable);
+        final Map<Throwable, Context.Metadata> metadataByThrowable = new 
IdentityHashMap<>();
+        Context.Metadata.populateMetadata(
+                metadataByThrowable, Collections.newSetFromMap(new 
IdentityHashMap<>()), null, throwable);
         return (C) new Context(0, metadataByThrowable);
     }
 
@@ -292,8 +295,9 @@ 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);
+                final Map<Throwable, Metadata> metadataByThrowable = new 
IdentityHashMap<>();
+                populateMetadata(
+                        metadataByThrowable, Collections.newSetFromMap(new 
IdentityHashMap<>()), null, throwable);
                 return metadataByThrowable;
             }
 

Reply via email to