Skip to content

Commit 2bc484c

Browse files
authored
Handle missing stack traces in ExtendedThreadInformation (#3655)
Fixes `ArrayIndexOutOfBoundsException` on invocation of `Message#getFormattedMessage()` when any thread has no stack trace, which occurs on some JVM implementations.
1 parent cd70cbf commit 2bc484c

File tree

3 files changed

+73
-11
lines changed

3 files changed

+73
-11
lines changed

log4j-core-test/src/test/java/org/apache/logging/log4j/core/message/ExtendedThreadInformationTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,17 @@
1616
*/
1717
package org.apache.logging.log4j.core.message;
1818

19+
import static org.hamcrest.CoreMatchers.containsString;
20+
import static org.hamcrest.MatcherAssert.assertThat;
1921
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.when;
2024

25+
import java.lang.management.ThreadInfo;
2126
import org.apache.logging.log4j.message.ThreadDumpMessage;
2227
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.EnumSource;
2330

2431
/**
2532
* Tests that ThreadDumpMessage uses ExtendedThreadInformation when available.
@@ -33,4 +40,42 @@ void testMessage() {
3340
// System.out.print(message);
3441
assertTrue(message.contains(" Id="), "No header");
3542
}
43+
44+
@ParameterizedTest
45+
@EnumSource(Thread.State.class)
46+
void testMessageWithNullStackTrace(final Thread.State state) {
47+
obtainMessageWithMissingStackTrace(state, null);
48+
}
49+
50+
@ParameterizedTest
51+
@EnumSource(Thread.State.class)
52+
void testMessageWithEmptyStackTrace(final Thread.State state) {
53+
obtainMessageWithMissingStackTrace(state, new StackTraceElement[0]);
54+
}
55+
56+
private void obtainMessageWithMissingStackTrace(final Thread.State state, final StackTraceElement[] stackTrace) {
57+
// setup
58+
final String threadName = "the thread name";
59+
final long threadId = 23523L;
60+
61+
final ThreadInfo threadInfo = mock(ThreadInfo.class);
62+
when(threadInfo.getStackTrace()).thenReturn(stackTrace);
63+
when(threadInfo.getThreadName()).thenReturn(threadName);
64+
when(threadInfo.getThreadId()).thenReturn(threadId);
65+
when(threadInfo.isSuspended()).thenReturn(true);
66+
when(threadInfo.isInNative()).thenReturn(true);
67+
when(threadInfo.getThreadState()).thenReturn(state);
68+
69+
// given
70+
final ExtendedThreadInformation sut = new ExtendedThreadInformation(threadInfo);
71+
72+
// when
73+
final StringBuilder result = new StringBuilder();
74+
sut.printThreadInfo(result);
75+
76+
// then
77+
assertThat(result.toString(), containsString(threadName));
78+
assertThat(result.toString(), containsString(state.name()));
79+
assertThat(result.toString(), containsString(String.valueOf(threadId)));
80+
}
3681
}

log4j-core/src/main/java/org/apache/logging/log4j/core/message/ExtendedThreadInformation.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.lang.management.ThreadInfo;
2424
import org.apache.logging.log4j.message.ThreadInformation;
2525
import org.apache.logging.log4j.util.StringBuilders;
26+
import org.jspecify.annotations.Nullable;
2627

2728
/**
2829
* Provides information on locks and monitors in the thread dump. This class requires Java 1.6 to compile and
@@ -119,17 +120,17 @@ private void formatState(final StringBuilder sb, final ThreadInfo info) {
119120
break;
120121
}
121122
case WAITING: {
122-
final StackTraceElement element = info.getStackTrace()[0];
123-
final String className = element.getClassName();
124-
final String method = element.getMethodName();
125-
if (className.equals("java.lang.Object") && method.equals("wait")) {
123+
final StackTraceElement element = getFirstStackTraceElement(info);
124+
final String className = element != null ? element.getClassName() : null;
125+
final String method = element != null ? element.getMethodName() : null;
126+
if ("java.lang.Object".equals(className) && "wait".equals(method)) {
126127
sb.append(" (on object monitor");
127128
if (info.getLockOwnerName() != null) {
128129
sb.append(" owned by \"");
129130
sb.append(info.getLockOwnerName()).append("\" Id=").append(info.getLockOwnerId());
130131
}
131132
sb.append(')');
132-
} else if (className.equals("java.lang.Thread") && method.equals("join")) {
133+
} else if ("java.lang.Thread".equals(className) && "join".equals(method)) {
133134
sb.append(" (on completion of thread ")
134135
.append(info.getLockOwnerId())
135136
.append(')');
@@ -144,19 +145,19 @@ private void formatState(final StringBuilder sb, final ThreadInfo info) {
144145
break;
145146
}
146147
case TIMED_WAITING: {
147-
final StackTraceElement element = info.getStackTrace()[0];
148-
final String className = element.getClassName();
149-
final String method = element.getMethodName();
150-
if (className.equals("java.lang.Object") && method.equals("wait")) {
148+
final StackTraceElement element = getFirstStackTraceElement(info);
149+
final String className = element != null ? element.getClassName() : null;
150+
final String method = element != null ? element.getMethodName() : null;
151+
if ("java.lang.Object".equals(className) && "wait".equals(method)) {
151152
sb.append(" (on object monitor");
152153
if (info.getLockOwnerName() != null) {
153154
sb.append(" owned by \"");
154155
sb.append(info.getLockOwnerName()).append("\" Id=").append(info.getLockOwnerId());
155156
}
156157
sb.append(')');
157-
} else if (className.equals("java.lang.Thread") && method.equals("sleep")) {
158+
} else if ("java.lang.Thread".equals(className) && "sleep".equals(method)) {
158159
sb.append(" (sleeping)");
159-
} else if (className.equals("java.lang.Thread") && method.equals("join")) {
160+
} else if ("java.lang.Thread".equals(className) && "join".equals(method)) {
160161
sb.append(" (on completion of thread ")
161162
.append(info.getLockOwnerId())
162163
.append(')');
@@ -174,4 +175,10 @@ private void formatState(final StringBuilder sb, final ThreadInfo info) {
174175
break;
175176
}
176177
}
178+
179+
@Nullable
180+
private static StackTraceElement getFirstStackTraceElement(final ThreadInfo info) {
181+
final StackTraceElement[] stackTrace = info.getStackTrace();
182+
return stackTrace != null && stackTrace.length > 0 ? stackTrace[0] : null;
183+
}
177184
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="https://logging.apache.org/xml/ns"
4+
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
5+
type="fixed">
6+
<issue id="3655" link="https://github.com/apache/logging-log4j2/pull/3655"/>
7+
<description format="asciidoc">
8+
Fix `ArrayIndexOutOfBoundsException` on invocation of `Message.getFormattedMessage()` when any thread has no stack trace, which occurs on some JVM implementations.
9+
</description>
10+
</entry>

0 commit comments

Comments
 (0)