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; }
