From e0e4f69d4ce1a38ff5844c9383e3192b8c2304ff Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Tue, 14 Oct 2025 11:25:47 +0200 Subject: [PATCH 1/3] New test case demonstrating ArrayIndexOutOfBoundsException if concurrently modifying the stacktrace --- .../ThrowablePatternConverterTest.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) 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 c893947423c..eee71383e1a 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 @@ -29,6 +29,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.LockSupport; import java.util.stream.Collectors; @@ -443,10 +444,51 @@ private static String normalizeStackTrace(final String stackTrace, final String .replaceAll(" ~\\[\\?:[^]]+](\\Q" + conversionEnding + "\\E|$)", " ~[?:0]$1"); } + @Test + @Issue("https://github.com/apache/logging-log4j2/issues/3940") + void concurrent_stacktrace_mutation_should_not_cause_failure() throws Exception { + final ExecutorService executor = Executors.newFixedThreadPool(2); + + // Create the formatter + final List patternFormatters = PATTERN_PARSER.parse(patternPrefix, false, true, true); + assertThat(patternFormatters).hasSize(1); + final PatternFormatter patternFormatter = patternFormatters.get(0); + final StringBuilder buffer = new StringBuilder(); + + try { + for (int i = 0; i < 10; i++) { + final CountDownLatch latch = new CountDownLatch(2); + final Throwable exception = new RuntimeException(); + final LogEvent logEvent = Log4jLogEvent.newBuilder() + .setThrown(exception) + .setLevel(LEVEL) + .build(); + + executor.submit(() -> { + try { + patternFormatter.format(logEvent, buffer); + buffer.setLength(0); + latch.countDown(); + } catch (Throwable e) { + e.printStackTrace(); + } + }); + executor.submit(() -> { + exception.setStackTrace(new StackTraceElement[0]); + latch.countDown(); + }); + if (latch.await(1, TimeUnit.SECONDS) == false) { + throw new IllegalStateException("timed out waiting for tasks to complete"); + } + } + } finally { + executor.shutdownNow(); + } + } + @RepeatedTest(10) @Issue("https://github.com/apache/logging-log4j2/issues/3929") void concurrent_suppressed_mutation_should_not_cause_failure() throws Exception { - // Test constants final int threadCount = Math.max(8, Runtime.getRuntime().availableProcessors()); final ExecutorService executor = Executors.newFixedThreadPool(threadCount); From 2231df574b37143686fa0e52cc30510682594ec9 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Tue, 14 Oct 2025 11:27:05 +0200 Subject: [PATCH 2/3] Fix ArrayIndexOutOfBoundsException if concurrently modifying the stacktrace of a throwablen while rendering it --- .../RootThrowablePatternConverterTest.java | 3 ++- .../ThrowableExtendedStackTraceRenderer.java | 2 +- .../ThrowableInvertedStackTraceRenderer.java | 2 +- .../pattern/ThrowableStackTraceRenderer.java | 19 ++++++++++++++----- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java index 80e4f008bf5..344fc315086 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java @@ -17,7 +17,6 @@ package org.apache.logging.log4j.core.pattern; import static java.util.Arrays.asList; -import static org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.EXCEPTION; import static org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.LEVEL; import static org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.convert; import static org.assertj.core.api.Assertions.assertThat; @@ -38,6 +37,8 @@ */ class RootThrowablePatternConverterTest { + static final Throwable EXCEPTION = TestFriendlyException.INSTANCE; + private static final StackTraceElement THROWING_METHOD = Throwables.getRootCause(EXCEPTION).getStackTrace()[0]; 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 06dbd4904ab..c70e57d4d46 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 @@ -132,7 +132,7 @@ private static Map createClassResourceInfoByName( Class executionStackTraceElementClass = executionStackTrace.isEmpty() ? null : executionStackTrace.peekLast(); ClassLoader lastLoader = null; - final StackTraceElement[] stackTraceElements = throwable.getStackTrace(); + final StackTraceElement[] stackTraceElements = metadata.stackTrace; for (int throwableStackIndex = metadata.stackLength - 1; throwableStackIndex >= 0; --throwableStackIndex) { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java index f1d26797fa4..ca8a28ea1cb 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java @@ -74,7 +74,7 @@ private void renderThrowable( } renderThrowableMessage(buffer, throwable); buffer.append(lineSeparator); - renderStackTraceElements(buffer, throwable, context, metadata, prefix, lineSeparator); + renderStackTraceElements(buffer, context, metadata, prefix, lineSeparator); renderSuppressed(buffer, metadata.suppressed, context, visitedThrowables, prefix + '\t', lineSeparator); } } 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 301f6efccec..5769ce5b65e 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 @@ -98,7 +98,7 @@ private void renderThrowable( final Context.Metadata metadata = context.metadataByThrowable.get(throwable); renderThrowableMessage(buffer, throwable); buffer.append(lineSeparator); - renderStackTraceElements(buffer, throwable, context, metadata, prefix, lineSeparator); + renderStackTraceElements(buffer, context, metadata, prefix, lineSeparator); renderSuppressed(buffer, metadata.suppressed, context, visitedThrowables, prefix + '\t', lineSeparator); renderCause(buffer, throwable.getCause(), context, visitedThrowables, prefix, lineSeparator); } @@ -148,13 +148,12 @@ static void renderThrowableMessage(final StringBuilder buffer, final Throwable t final void renderStackTraceElements( final StringBuilder buffer, - final Throwable throwable, final C context, final Context.Metadata metadata, final String prefix, final String lineSeparator) { context.ignoredStackTraceElementCount = 0; - final StackTraceElement[] stackTraceElements = throwable.getStackTrace(); + final StackTraceElement[] stackTraceElements = metadata.stackTrace; for (int i = 0; i < metadata.stackLength; i++) { renderStackTraceElement(buffer, stackTraceElements[i], context, prefix, lineSeparator); } @@ -268,6 +267,11 @@ static final class Metadata { */ final int stackLength; + /** + * The stack trace of this {@link Throwable} + */ + final StackTraceElement[] stackTrace; + /** * The suppressed exceptions attached to this {@link Throwable}. * This needs to be captured separately since {@link Throwable#getSuppressed()} can change. @@ -277,9 +281,14 @@ static final class Metadata { */ final Throwable[] suppressed; - private Metadata(final int commonElementCount, final int stackLength, final Throwable[] suppressed) { + private Metadata( + final int commonElementCount, + final int stackLength, + final StackTraceElement[] stackTrace, + final Throwable[] suppressed) { this.commonElementCount = commonElementCount; this.stackLength = stackLength; + this.stackTrace = stackTrace; this.suppressed = suppressed; } @@ -339,7 +348,7 @@ private static Metadata populateMetadata( commonElementCount = 0; stackLength = currentTrace.length; } - return new Metadata(commonElementCount, stackLength, suppressed); + return new Metadata(commonElementCount, stackLength, currentTrace, suppressed); } } } From b0c4ce95bcea448c031130b5affa2e8cf994da74 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Tue, 14 Oct 2025 11:34:33 +0200 Subject: [PATCH 3/3] Add changelog entry --- ...kTraceRenderer_ArrayIndexOutOfBoundsException.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/changelog/2.25.2/3940_ThrowableStackTraceRenderer_ArrayIndexOutOfBoundsException.xml diff --git a/src/changelog/2.25.2/3940_ThrowableStackTraceRenderer_ArrayIndexOutOfBoundsException.xml b/src/changelog/2.25.2/3940_ThrowableStackTraceRenderer_ArrayIndexOutOfBoundsException.xml new file mode 100644 index 00000000000..8d2a15f5b1a --- /dev/null +++ b/src/changelog/2.25.2/3940_ThrowableStackTraceRenderer_ArrayIndexOutOfBoundsException.xml @@ -0,0 +1,12 @@ + + + + + Fixes `ArrayIndexOutOfBoundsException` thrown by `ThrowableStackTraceRenderer` when a `Throwable`'s stacktrace is mutated concurrently. + +