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 9f3b57983e Fix circular references in exceptions (#4249)
9f3b57983e is described below

commit 9f3b57983e02481aa7cb1c19983755e74f762358
Author: Volkan Yazıcı <[email protected]>
AuthorDate: Wed Sep 2 13:53:29 2026 +0200

    Fix circular references in exceptions (#4249)
    
    * Fix circular references in exceptions
---
 .../appender/nosql/NoSqlDatabaseManagerTest.java   | 25 +++++++++++
 .../ExtendedThrowablePatternConverterTest.java     | 48 ++++++++++++++++++++++
 .../pattern/ThrowablePatternConverterTest.java     |  2 +-
 .../logging/log4j/core/util/ThrowablesTest.java    |  9 ++++
 .../core/appender/nosql/NoSqlDatabaseManager.java  | 10 ++++-
 .../ThrowableExtendedStackTraceRenderer.java       |  4 +-
 .../apache/logging/log4j/core/util/Throwables.java |  5 ++-
 .../jpa/converter/ThrowableAttributeConverter.java | 20 ++++++---
 .../converter/ThrowableAttributeConverterTest.java | 14 +++++++
 .../.2.x.x/4249_fix-circular-exception.xml         | 13 ++++++
 10 files changed, 137 insertions(+), 13 deletions(-)

diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java
index e9c53a3706..d53769e8b8 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java
@@ -25,6 +25,7 @@ import static org.mockito.BDDMockito.given;
 import static org.mockito.BDDMockito.then;
 import static org.mockito.Mockito.mock;
 
+import foo.TestFriendlyException;
 import java.io.IOException;
 import java.sql.SQLException;
 import java.util.Collection;
@@ -428,4 +429,28 @@ public class NoSqlDatabaseManagerTest {
             assertEquals("The context stack is not correct.", stack.asList(), 
object.get("contextStack"));
         }
     }
+
+    @Test
+    public void testWriteInternalWithCyclicCause() {
+        given(connection.isClosed()).willReturn(false);
+        final Throwable exception = TestFriendlyException.INSTANCE;
+
+        try (final NoSqlDatabaseManager<?> manager =
+                NoSqlDatabaseManager.getNoSqlDatabaseManager("name", 0, 
provider, null, null)) {
+            manager.startup();
+            manager.connectAndStart();
+            manager.writeInternal(
+                    Log4jLogEvent.newBuilder().setThrown(exception).build(), 
null);
+
+            then(connection).should().insertObject(captor.capture());
+            final Map<String, Object> thrown =
+                    (Map<String, Object>) 
captor.getValue().unwrap().get("thrown");
+            final Map<String, Object> cause = (Map<String, Object>) 
thrown.get("cause");
+            final Map<String, Object> nestedCause = (Map<String, Object>) 
cause.get("cause");
+            assertEquals(exception.getMessage(), thrown.get("message"));
+            assertEquals(exception.getCause().getMessage(), 
cause.get("message"));
+            assertEquals(exception.getCause().getCause().getMessage(), 
nestedCause.get("message"));
+            assertNull("The cycle should not be serialized.", 
nestedCause.get("cause"));
+        }
+    }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
index edffb79bc2..7f2f16132f 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
@@ -18,6 +18,7 @@ package org.apache.logging.log4j.core.pattern;
 
 import static java.util.Arrays.asList;
 import static 
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.THROWING_METHOD;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import foo.TestFriendlyException;
 import java.util.List;
@@ -25,6 +26,7 @@ import 
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.Abstr
 import 
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.AbstractStackTraceTest;
 import 
org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.DepthTestCase;
 import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
@@ -33,6 +35,52 @@ import org.junit.jupiter.params.provider.MethodSource;
  */
 class ExtendedThrowablePatternConverterTest {
 
+    /**
+     * Minimal equality-colliding exception for exercising causal-chain 
traversal.
+     *
+     * <p>{@link TestFriendlyException} is unsuitable here because its shared
+     * instance has a larger graph with suppressed exceptions and circular
+     * references, which prevents this test from controlling the two cause
+     * stack traces independently.
+     */
+    private static final class CollidingException extends RuntimeException {
+
+        private CollidingException(final String message) {
+            super(message);
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            return obj instanceof CollidingException;
+        }
+
+        @Override
+        public int hashCode() {
+            return 0;
+        }
+    }
+
+    @Test
+    void colliding_causes_should_resolve_extended_stack_trace_classes() {
+        final Throwable root = new CollidingException("root");
+        final Throwable cause = new CollidingException("cause");
+        root.initCause(cause);
+        root.setStackTrace(new StackTraceElement[] {
+            new StackTraceElement(
+                    ExtendedThrowablePatternConverterTest.class.getName(),
+                    "test",
+                    "ExtendedThrowablePatternConverterTest.java",
+                    1)
+        });
+        final String causeClassName = TestFriendlyException.class.getName();
+        cause.setStackTrace(new StackTraceElement[] {
+            new StackTraceElement(causeClassName, "create", 
"TestFriendlyException.java", 1)
+        });
+
+        assertThat(ThrowablePatternConverterTest.convert("%xEx", root))
+                .contains("\tat " + causeClassName + 
".create(TestFriendlyException.java:1) ~[test-classes/:?]");
+    }
+
     @Nested
     class PropertyTest extends AbstractPropertyTest {
 
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
index 20875a0e3e..17a9812b2f 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
@@ -580,7 +580,7 @@ public class ThrowablePatternConverterTest {
         return convert(pattern, EXCEPTION);
     }
 
-    private static String convert(final String pattern, final Throwable 
throwable) {
+    static String convert(final String pattern, final Throwable throwable) {
         final List<PatternFormatter> patternFormatters = 
PATTERN_PARSER.parse(pattern, false, true, true);
         final LogEvent logEvent =
                 
Log4jLogEvent.newBuilder().setThrown(throwable).setLevel(LEVEL).build();
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
index 912d62a1d9..b1a2370aae 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
@@ -17,8 +17,10 @@
 package org.apache.logging.log4j.core.util;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import foo.TestFriendlyException;
 import org.junit.jupiter.api.Test;
 
 class ThrowablesTest {
@@ -53,6 +55,13 @@ class ThrowablesTest {
         assertEquals(cause1, Throwables.getRootCause(cause3));
     }
 
+    @Test
+    void testGetRootCauseWithCollidingExceptions() {
+        final Throwable throwable = TestFriendlyException.INSTANCE;
+        final Throwable rootCause = throwable.getCause().getCause();
+        assertSame(rootCause, Throwables.getRootCause(throwable));
+    }
+
     @Test
     void testRethrowRuntimeException() {
         assertThrows(NullPointerException.class, () -> Throwables.rethrow(new 
NullPointerException()));
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java
index e053944a0c..edfa07178c 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java
@@ -17,7 +17,10 @@
 package org.apache.logging.log4j.core.appender.nosql;
 
 import java.io.Serializable;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Stream;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
@@ -223,8 +226,11 @@ public final class NoSqlDatabaseManager<W> extends 
AbstractDatabaseManager {
             exceptionEntity.set("type", thrown.getClass().getName());
             exceptionEntity.set("message", thrown.getMessage());
             exceptionEntity.set("stackTrace", 
this.convertStackTrace(thrown.getStackTrace()));
-            while (thrown.getCause() != null) {
-                thrown = thrown.getCause();
+            final Set<Throwable> visitedThrowables = 
Collections.newSetFromMap(new IdentityHashMap<>());
+            visitedThrowables.add(thrown);
+            Throwable cause;
+            while ((cause = thrown.getCause()) != null && 
visitedThrowables.add(cause)) {
+                thrown = cause;
                 final NoSqlObject<W> causingExceptionEntity = 
this.connection.createObject();
                 causingExceptionEntity.set("type", 
thrown.getClass().getName());
                 causingExceptionEntity.set("message", thrown.getMessage());
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRenderer.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRenderer.java
index 2c7ea334bb..46dc37a3c8 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRenderer.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRenderer.java
@@ -20,7 +20,7 @@ import java.util.ArrayDeque;
 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.Queue;
@@ -112,7 +112,7 @@ final class ThrowableExtendedStackTraceRenderer
             final Map<String, ClassResourceInfo> classResourceInfoByName = new 
HashMap<>();
 
             // Walk over the causal chain
-            final Set<Throwable> visitedThrowables = new HashSet<>();
+            final Set<Throwable> visitedThrowables = 
Collections.newSetFromMap(new IdentityHashMap<>());
             final Queue<Throwable> pendingThrowables = new 
ArrayDeque<>(Collections.singleton(rootThrowable));
             Throwable throwable;
             while ((throwable = pendingThrowables.poll()) != null && 
visitedThrowables.add(throwable)) {
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
index cf00631f9f..a673000674 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
@@ -25,7 +25,8 @@ import java.io.PrintWriter;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Set;
 import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings;
@@ -46,7 +47,7 @@ public final class Throwables {
      */
     public static Throwable getRootCause(final Throwable throwable) {
         requireNonNull(throwable, "throwable");
-        final Set<Throwable> visitedThrowables = new HashSet<>();
+        final Set<Throwable> visitedThrowables = Collections.newSetFromMap(new 
IdentityHashMap<>());
         Throwable prevCause = throwable;
         visitedThrowables.add(prevCause);
         Throwable nextCause;
diff --git 
a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java
 
b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java
index 693d05dcf2..1448d3b287 100644
--- 
a/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java
+++ 
b/log4j-jpa/src/main/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverter.java
@@ -20,8 +20,11 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.Set;
 import javax.persistence.AttributeConverter;
 import javax.persistence.Converter;
 import org.apache.logging.log4j.util.LoaderUtil;
@@ -64,13 +67,18 @@ public class ThrowableAttributeConverter implements 
AttributeConverter<Throwable
     }
 
     private void convertThrowable(final StringBuilder builder, final Throwable 
throwable) {
-        builder.append(throwable.toString()).append('\n');
-        for (final StackTraceElement element : throwable.getStackTrace()) {
-            builder.append("\tat ").append(element).append('\n');
-        }
-        if (throwable.getCause() != null) {
+        final Set<Throwable> visitedThrowables = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        for (Throwable currentThrowable = throwable; 
visitedThrowables.add(currentThrowable); ) {
+            builder.append(currentThrowable).append('\n');
+            for (final StackTraceElement element : 
currentThrowable.getStackTrace()) {
+                builder.append("\tat ").append(element).append('\n');
+            }
+            final Throwable cause = currentThrowable.getCause();
+            if (cause == null || visitedThrowables.contains(cause)) {
+                break;
+            }
             builder.append("Caused by ");
-            this.convertThrowable(builder, throwable.getCause());
+            currentThrowable = cause;
         }
     }
 
diff --git 
a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverterTest.java
 
b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverterTest.java
index f57d1762ed..5ad40fd144 100644
--- 
a/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverterTest.java
+++ 
b/log4j-jpa/src/test/java/org/apache/logging/log4j/core/appender/db/jpa/converter/ThrowableAttributeConverterTest.java
@@ -19,8 +19,10 @@ package 
org.apache.logging.log4j.core.appender.db.jpa.converter;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.sql.SQLException;
+import org.apache.commons.lang3.StringUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -70,6 +72,18 @@ class ThrowableAttributeConverterTest {
         assertEquals(stackTrace, getStackTrace(reversed), "The reversed value 
is not correct.");
     }
 
+    @Test
+    void testConvertCyclicCause() {
+        final Exception exception1 = new Exception("exception1");
+        final Exception exception2 = new Exception("exception2");
+        exception1.initCause(exception2);
+        exception2.initCause(exception1);
+        final String converted = converter.convertToDatabaseColumn(exception1);
+        assertTrue(converted.contains("exception1"));
+        assertTrue(converted.contains("exception2"));
+        assertEquals(1, StringUtils.countMatches(converted, "Caused by "));
+    }
+
     @Test
     void testConvertNullToDatabaseColumn() {
         assertNull(this.converter.convertToDatabaseColumn(null), "The 
converted value should be null.");
diff --git a/src/changelog/.2.x.x/4249_fix-circular-exception.xml 
b/src/changelog/.2.x.x/4249_fix-circular-exception.xml
new file mode 100644
index 0000000000..580e175138
--- /dev/null
+++ b/src/changelog/.2.x.x/4249_fix-circular-exception.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="fixed">
+    <issue id="3933" 
link="https://github.com/apache/logging-log4j2/issues/3933"/>
+    <issue id="4249" 
link="https://github.com/apache/logging-log4j2/pull/4249"/>
+    <description format="asciidoc">
+        Fix `Throwable` causal-chain handling for cyclic and 
identity-malfunctioning exceptions
+    </description>
+</entry>

Reply via email to