Skip to content

Commit 7125d93

Browse files
joepembevy
andauthored
Fix NPE in ThrowableStackTraceRenderer due to concurrent mutation of suppressed exceptions (#3934)
Co-authored-by: Volkan Yazıcı <volkan@yazi.ci>
1 parent 84c7716 commit 7125d93

File tree

4 files changed

+105
-12
lines changed

4 files changed

+105
-12
lines changed

log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,21 @@
2626
import java.nio.charset.Charset;
2727
import java.nio.charset.StandardCharsets;
2828
import java.util.List;
29+
import java.util.concurrent.CountDownLatch;
30+
import java.util.concurrent.ExecutorService;
31+
import java.util.concurrent.Executors;
32+
import java.util.concurrent.atomic.AtomicInteger;
33+
import java.util.concurrent.locks.LockSupport;
2934
import java.util.stream.Collectors;
35+
import java.util.stream.IntStream;
3036
import java.util.stream.Stream;
3137
import org.apache.logging.log4j.Level;
3238
import org.apache.logging.log4j.core.LogEvent;
3339
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
3440
import org.apache.logging.log4j.core.layout.PatternLayout;
3541
import org.jspecify.annotations.Nullable;
3642
import org.junit.jupiter.api.Nested;
43+
import org.junit.jupiter.api.RepeatedTest;
3744
import org.junit.jupiter.api.Test;
3845
import org.junit.jupiter.params.ParameterizedTest;
3946
import org.junit.jupiter.params.provider.MethodSource;
@@ -435,6 +442,66 @@ private static String normalizeStackTrace(final String stackTrace, final String
435442
// We replace the `~[?:1.8.0_422]` suffix of such classes with `~[?:0]`.
436443
.replaceAll(" ~\\[\\?:[^]]+](\\Q" + conversionEnding + "\\E|$)", " ~[?:0]$1");
437444
}
445+
446+
@RepeatedTest(10)
447+
@Issue("https://github.com/apache/logging-log4j2/issues/3929")
448+
void concurrent_suppressed_mutation_should_not_cause_failure() throws Exception {
449+
450+
// Test constants
451+
final int threadCount = Math.max(8, Runtime.getRuntime().availableProcessors());
452+
final ExecutorService executor = Executors.newFixedThreadPool(threadCount);
453+
final Exception exception = new Exception();
454+
final CountDownLatch startLatch =
455+
new CountDownLatch(threadCount + /* the main thread invoking the rendering: */ 1);
456+
final int exceptionPerThreadCount = 100;
457+
final AtomicInteger runningThreadCountRef = new AtomicInteger(threadCount);
458+
459+
// Schedule threads that will start adding suppressed exceptions with the start signal
460+
for (int threadIndex = 0; threadIndex < threadCount; threadIndex++) {
461+
final int threadIndex_ = threadIndex;
462+
executor.submit(() -> {
463+
try {
464+
List<Exception> exceptions = IntStream.range(0, exceptionPerThreadCount)
465+
.mapToObj(exceptionIndex -> new Exception(threadIndex_ + "-" + exceptionIndex))
466+
.collect(Collectors.toList());
467+
startLatch.countDown();
468+
startLatch.await();
469+
for (int exceptionIndex = 0; exceptionIndex < exceptionPerThreadCount; exceptionIndex++) {
470+
exception.addSuppressed(exceptions.get(exceptionIndex));
471+
// Give some time slack to increase randomness
472+
LockSupport.parkNanos(1);
473+
}
474+
} catch (InterruptedException ignored) {
475+
// Restore the interrupt
476+
Thread.currentThread().interrupt();
477+
} finally {
478+
runningThreadCountRef.decrementAndGet();
479+
}
480+
});
481+
}
482+
483+
// Create the formatter
484+
final List<PatternFormatter> patternFormatters = PATTERN_PARSER.parse(patternPrefix, false, true, true);
485+
assertThat(patternFormatters).hasSize(1);
486+
final PatternFormatter patternFormatter = patternFormatters.get(0);
487+
488+
// Create the log event and the layout buffer
489+
final LogEvent logEvent = Log4jLogEvent.newBuilder()
490+
.setThrown(exception)
491+
.setLevel(LEVEL)
492+
.build();
493+
final StringBuilder buffer = new StringBuilder(16384);
494+
495+
// Trigger the start latch and format the exception
496+
startLatch.countDown();
497+
startLatch.await();
498+
while (runningThreadCountRef.get() > 0) {
499+
// Give some time slack to increase randomness
500+
LockSupport.parkNanos(1);
501+
patternFormatter.format(logEvent, buffer);
502+
buffer.setLength(0);
503+
}
504+
}
438505
}
439506

440507
static String convert(final String pattern) {

log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private void renderThrowable(
6060
buffer.append(']');
6161
buffer.append(lineSeparator);
6262
} else {
63+
final Context.Metadata metadata = context.metadataByThrowable.get(throwable);
6364
lineCapacityAcquired = renderCause(
6465
buffer,
6566
throwable.getCause(),
@@ -73,9 +74,8 @@ private void renderThrowable(
7374
}
7475
renderThrowableMessage(buffer, throwable);
7576
buffer.append(lineSeparator);
76-
renderStackTraceElements(buffer, throwable, context, prefix, lineSeparator);
77-
renderSuppressed(
78-
buffer, throwable.getSuppressed(), context, visitedThrowables, prefix + '\t', lineSeparator);
77+
renderStackTraceElements(buffer, throwable, context, metadata, prefix, lineSeparator);
78+
renderSuppressed(buffer, metadata.suppressed, context, visitedThrowables, prefix + '\t', lineSeparator);
7979
}
8080
}
8181

log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ private void renderThrowable(
9595
buffer.append(']');
9696
buffer.append(lineSeparator);
9797
} else {
98+
final Context.Metadata metadata = context.metadataByThrowable.get(throwable);
9899
renderThrowableMessage(buffer, throwable);
99100
buffer.append(lineSeparator);
100-
renderStackTraceElements(buffer, throwable, context, prefix, lineSeparator);
101-
renderSuppressed(
102-
buffer, throwable.getSuppressed(), context, visitedThrowables, prefix + '\t', lineSeparator);
101+
renderStackTraceElements(buffer, throwable, context, metadata, prefix, lineSeparator);
102+
renderSuppressed(buffer, metadata.suppressed, context, visitedThrowables, prefix + '\t', lineSeparator);
103103
renderCause(buffer, throwable.getCause(), context, visitedThrowables, prefix, lineSeparator);
104104
}
105105
}
@@ -150,10 +150,10 @@ final void renderStackTraceElements(
150150
final StringBuilder buffer,
151151
final Throwable throwable,
152152
final C context,
153+
final Context.Metadata metadata,
153154
final String prefix,
154155
final String lineSeparator) {
155156
context.ignoredStackTraceElementCount = 0;
156-
final Context.Metadata metadata = context.metadataByThrowable.get(throwable);
157157
final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
158158
for (int i = 0; i < metadata.stackLength; i++) {
159159
renderStackTraceElement(buffer, stackTraceElements[i], context, prefix, lineSeparator);
@@ -268,9 +268,19 @@ static final class Metadata {
268268
*/
269269
final int stackLength;
270270

271-
private Metadata(final int commonElementCount, final int stackLength) {
271+
/**
272+
* The suppressed exceptions attached to this {@link Throwable}.
273+
* This needs to be captured separately since {@link Throwable#getSuppressed()} can change.
274+
*
275+
* @see <a href="https://github.com/apache/logging-log4j2/issues/3929">#3929</a>
276+
* @see <a href="https://github.com/apache/logging-log4j2/pull/3934">#3934</a>
277+
*/
278+
final Throwable[] suppressed;
279+
280+
private Metadata(final int commonElementCount, final int stackLength, final Throwable[] suppressed) {
272281
this.commonElementCount = commonElementCount;
273282
this.stackLength = stackLength;
283+
this.suppressed = suppressed;
274284
}
275285

276286
static Map<Throwable, Metadata> ofThrowable(final Throwable throwable) {
@@ -288,11 +298,12 @@ private static void populateMetadata(
288298
// Populate metadata of the current throwable
289299
@Nullable
290300
final StackTraceElement[] rootTrace = parentThrowable == null ? null : parentThrowable.getStackTrace();
291-
final Metadata metadata = populateMetadata(rootTrace, throwable.getStackTrace());
301+
final Metadata metadata =
302+
populateMetadata(rootTrace, throwable.getStackTrace(), throwable.getSuppressed());
292303
metadataByThrowable.put(throwable, metadata);
293304

294305
// Populate metadata of suppressed exceptions
295-
for (final Throwable suppressed : throwable.getSuppressed()) {
306+
for (final Throwable suppressed : metadata.suppressed) {
296307
if (!visitedThrowables.contains(suppressed)) {
297308
visitedThrowables.add(suppressed);
298309
populateMetadata(metadataByThrowable, visitedThrowables, throwable, suppressed);
@@ -308,7 +319,9 @@ private static void populateMetadata(
308319
}
309320

310321
private static Metadata populateMetadata(
311-
@Nullable final StackTraceElement[] parentTrace, final StackTraceElement[] currentTrace) {
322+
@Nullable final StackTraceElement[] parentTrace,
323+
final StackTraceElement[] currentTrace,
324+
final Throwable[] suppressed) {
312325
int commonElementCount;
313326
int stackLength;
314327
if (parentTrace != null) {
@@ -326,7 +339,7 @@ private static Metadata populateMetadata(
326339
commonElementCount = 0;
327340
stackLength = currentTrace.length;
328341
}
329-
return new Metadata(commonElementCount, stackLength);
342+
return new Metadata(commonElementCount, stackLength, suppressed);
330343
}
331344
}
332345
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns="https://logging.apache.org/xml/ns"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="
5+
https://logging.apache.org/xml/ns
6+
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
7+
type="fixed">
8+
<issue id="3929" link="https://github.com/apache/logging-log4j2/issues/3929"/>
9+
<issue id="3934" link="https://github.com/apache/logging-log4j2/pull/3934"/>
10+
<description format="asciidoc">
11+
Fixes `NullPointerException` thrown by `ThrowableStackTraceRenderer` when suppressed exceptions are mutated concurrently.
12+
</description>
13+
</entry>

0 commit comments

Comments
 (0)