From c9609925d68f4a75ca7fe167f988366201b413c1 Mon Sep 17 00:00:00 2001
From: CHANHAN <130114269+chanani@users.noreply.github.com>
Date: Thu, 19 Feb 2026 12:53:54 +0900
Subject: [PATCH 1/2] Catch `LinkageError` in
`ThrowableExtendedStackTraceRenderer#loadClass` (#4028)
Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com>
---
...rowableExtendedStackTraceRendererTest.java | 135 ++++++++++++++++++
.../ThrowableExtendedStackTraceRenderer.java | 2 +-
...in_ThrowableExtendedStackTraceRenderer.xml | 9 ++
3 files changed, 145 insertions(+), 1 deletion(-)
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java
create mode 100644 src/changelog/.2.x.x/4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java
new file mode 100644
index 00000000000..ca8b0330fb3
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.pattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+
+import java.util.List;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.Issue;
+
+/**
+ * Tests for {@link ThrowableExtendedStackTraceRenderer}.
+ *
+ *
Verifies that {@link NoClassDefFoundError} during class loading
+ * does not break the extended stack trace rendering ({@code %xEx}).
+ *
+ * @see #4028
+ */
+class ThrowableExtendedStackTraceRendererTest {
+
+ private static final PatternParser PATTERN_PARSER = PatternLayout.createPatternParser(null);
+
+ /**
+ * Verifies that the extended stack trace renderer does not propagate {@link NoClassDefFoundError}
+ * when a class in the stack trace cannot be found by the class loader.
+ */
+ @Test
+ @Issue("https://github.com/apache/logging-log4j2/issues/4028")
+ void loadClass_should_handle_NoClassDefFoundError() {
+
+ // Create an exception with a stack trace element referencing a non-existent class.
+ // When the extended renderer tries to resolve this class for JAR/version info,
+ // the class loader will throw NoClassDefFoundError.
+ final Exception exception = new Exception("test exception");
+ final StackTraceElement[] originalTrace = exception.getStackTrace();
+ final StackTraceElement[] modifiedTrace = new StackTraceElement[originalTrace.length + 1];
+ modifiedTrace[0] = new StackTraceElement(
+ "com.nonexistent.deliberately.missing.ClassName", "someMethod", "ClassName.java", 42);
+ System.arraycopy(originalTrace, 0, modifiedTrace, 1, originalTrace.length);
+ exception.setStackTrace(modifiedTrace);
+
+ // Render using the extended throwable pattern (%xEx)
+ final List patternFormatters = PATTERN_PARSER.parse("%xEx", false, true, true);
+ assertThat(patternFormatters).hasSize(1);
+ final PatternFormatter patternFormatter = patternFormatters.get(0);
+
+ final LogEvent logEvent = Log4jLogEvent.newBuilder()
+ .setThrown(exception)
+ .setLevel(Level.ERROR)
+ .build();
+ final StringBuilder buffer = new StringBuilder();
+
+ // The rendering should not throw any exception
+ assertThatNoException().isThrownBy(() -> patternFormatter.format(logEvent, buffer));
+
+ // The output should contain our non-existent class name and the exception message
+ final String output = buffer.toString();
+ assertThat(output).contains("com.nonexistent.deliberately.missing.ClassName");
+ assertThat(output).contains("test exception");
+ }
+
+ /**
+ * Verifies that the extended stack trace renderer gracefully handles a class loader
+ * that throws {@link NoClassDefFoundError} during class resolution.
+ *
+ * This simulates the real-world scenario from
+ * #4028,
+ * where custom class loaders (e.g., in application servers or frameworks like Frank!Framework)
+ * throw {@link NoClassDefFoundError} for classes that have been unloaded.
+ */
+ @Test
+ @Issue("https://github.com/apache/logging-log4j2/issues/4028")
+ void loadClass_should_handle_NoClassDefFoundError_from_custom_classloader() {
+
+ // Create a class loader that always throws NoClassDefFoundError
+ final ClassLoader errorThrowingLoader = new ClassLoader(null) {
+ @Override
+ public Class> loadClass(final String name) throws ClassNotFoundException {
+ throw new NoClassDefFoundError("Simulated missing class: " + name);
+ }
+ };
+
+ final Exception exception = new Exception("test with custom classloader");
+ exception.setStackTrace(new StackTraceElement[] {
+ new StackTraceElement("com.example.UnloadedClass", "doWork", "UnloadedClass.java", 10),
+ new StackTraceElement("com.example.CallerClass", "invoke", "CallerClass.java", 20)
+ });
+
+ final List patternFormatters = PATTERN_PARSER.parse("%xEx", false, true, true);
+ final PatternFormatter patternFormatter = patternFormatters.get(0);
+
+ final LogEvent logEvent = Log4jLogEvent.newBuilder()
+ .setThrown(exception)
+ .setLevel(Level.ERROR)
+ .build();
+ final StringBuilder buffer = new StringBuilder();
+
+ // Set the error-throwing class loader as the context class loader
+ // so the renderer's loadClass method encounters it
+ final Thread currentThread = Thread.currentThread();
+ final ClassLoader originalLoader = currentThread.getContextClassLoader();
+ try {
+ currentThread.setContextClassLoader(errorThrowingLoader);
+
+ // The rendering should succeed without propagating NoClassDefFoundError
+ assertThatNoException().isThrownBy(() -> patternFormatter.format(logEvent, buffer));
+ } finally {
+ currentThread.setContextClassLoader(originalLoader);
+ }
+
+ // The output should still contain the exception and stack trace
+ final String output = buffer.toString();
+ assertThat(output).contains("test with custom classloader");
+ assertThat(output).contains("com.example.UnloadedClass");
+ }
+}
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 c70e57d4d46..752cc772b9a 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
@@ -181,7 +181,7 @@ private static Class> loadClass(final ClassLoader loader, final String classNa
if (clazz != null) {
return clazz;
}
- } catch (final Exception ignored) {
+ } catch (final Exception | LinkageError ignored) {
// Do nothing
}
}
diff --git a/src/changelog/.2.x.x/4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml b/src/changelog/.2.x.x/4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
new file mode 100644
index 00000000000..6ec8de1421b
--- /dev/null
+++ b/src/changelog/.2.x.x/4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
@@ -0,0 +1,9 @@
+
+
+
+ Catch `LinkageError` in `ThrowableExtendedStackTraceRenderer#loadClass` to prevent `NoClassDefFoundError` from breaking logging
+
From 238c928c481420dc36b58f3e3398be573d80f282 Mon Sep 17 00:00:00 2001
From: CHANHAN <130114269+chanani@users.noreply.github.com>
Date: Sun, 1 Mar 2026 12:05:34 +0900
Subject: [PATCH 2/2] Catch Throwable in
ThrowableExtendedStackTraceRenderer#loadClass (#4028)
- Changed catch (Exception) to catch (Throwable) in loadClass method
- Moved tests into ThrowablePatternConverterTest.StackTraceTest
- Removed standalone ThrowableExtendedStackTraceRendererTest
- Updated changelog description and issue link per review feedback
Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com>
---
...rowableExtendedStackTraceRendererTest.java | 135 ------------------
.../ThrowablePatternConverterTest.java | 96 +++++++++++++
.../ThrowableExtendedStackTraceRenderer.java | 2 +-
...n_ThrowableExtendedStackTraceRenderer.xml} | 4 +-
4 files changed, 99 insertions(+), 138 deletions(-)
delete mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java
rename src/changelog/.2.x.x/{4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml => 4049_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml} (53%)
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java
deleted file mode 100644
index ca8b0330fb3..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRendererTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.pattern;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatNoException;
-
-import java.util.List;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.impl.Log4jLogEvent;
-import org.apache.logging.log4j.core.layout.PatternLayout;
-import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.Issue;
-
-/**
- * Tests for {@link ThrowableExtendedStackTraceRenderer}.
- *
- * Verifies that {@link NoClassDefFoundError} during class loading
- * does not break the extended stack trace rendering ({@code %xEx}).
- *
- * @see #4028
- */
-class ThrowableExtendedStackTraceRendererTest {
-
- private static final PatternParser PATTERN_PARSER = PatternLayout.createPatternParser(null);
-
- /**
- * Verifies that the extended stack trace renderer does not propagate {@link NoClassDefFoundError}
- * when a class in the stack trace cannot be found by the class loader.
- */
- @Test
- @Issue("https://github.com/apache/logging-log4j2/issues/4028")
- void loadClass_should_handle_NoClassDefFoundError() {
-
- // Create an exception with a stack trace element referencing a non-existent class.
- // When the extended renderer tries to resolve this class for JAR/version info,
- // the class loader will throw NoClassDefFoundError.
- final Exception exception = new Exception("test exception");
- final StackTraceElement[] originalTrace = exception.getStackTrace();
- final StackTraceElement[] modifiedTrace = new StackTraceElement[originalTrace.length + 1];
- modifiedTrace[0] = new StackTraceElement(
- "com.nonexistent.deliberately.missing.ClassName", "someMethod", "ClassName.java", 42);
- System.arraycopy(originalTrace, 0, modifiedTrace, 1, originalTrace.length);
- exception.setStackTrace(modifiedTrace);
-
- // Render using the extended throwable pattern (%xEx)
- final List patternFormatters = PATTERN_PARSER.parse("%xEx", false, true, true);
- assertThat(patternFormatters).hasSize(1);
- final PatternFormatter patternFormatter = patternFormatters.get(0);
-
- final LogEvent logEvent = Log4jLogEvent.newBuilder()
- .setThrown(exception)
- .setLevel(Level.ERROR)
- .build();
- final StringBuilder buffer = new StringBuilder();
-
- // The rendering should not throw any exception
- assertThatNoException().isThrownBy(() -> patternFormatter.format(logEvent, buffer));
-
- // The output should contain our non-existent class name and the exception message
- final String output = buffer.toString();
- assertThat(output).contains("com.nonexistent.deliberately.missing.ClassName");
- assertThat(output).contains("test exception");
- }
-
- /**
- * Verifies that the extended stack trace renderer gracefully handles a class loader
- * that throws {@link NoClassDefFoundError} during class resolution.
- *
- * This simulates the real-world scenario from
- * #4028,
- * where custom class loaders (e.g., in application servers or frameworks like Frank!Framework)
- * throw {@link NoClassDefFoundError} for classes that have been unloaded.
- */
- @Test
- @Issue("https://github.com/apache/logging-log4j2/issues/4028")
- void loadClass_should_handle_NoClassDefFoundError_from_custom_classloader() {
-
- // Create a class loader that always throws NoClassDefFoundError
- final ClassLoader errorThrowingLoader = new ClassLoader(null) {
- @Override
- public Class> loadClass(final String name) throws ClassNotFoundException {
- throw new NoClassDefFoundError("Simulated missing class: " + name);
- }
- };
-
- final Exception exception = new Exception("test with custom classloader");
- exception.setStackTrace(new StackTraceElement[] {
- new StackTraceElement("com.example.UnloadedClass", "doWork", "UnloadedClass.java", 10),
- new StackTraceElement("com.example.CallerClass", "invoke", "CallerClass.java", 20)
- });
-
- final List patternFormatters = PATTERN_PARSER.parse("%xEx", false, true, true);
- final PatternFormatter patternFormatter = patternFormatters.get(0);
-
- final LogEvent logEvent = Log4jLogEvent.newBuilder()
- .setThrown(exception)
- .setLevel(Level.ERROR)
- .build();
- final StringBuilder buffer = new StringBuilder();
-
- // Set the error-throwing class loader as the context class loader
- // so the renderer's loadClass method encounters it
- final Thread currentThread = Thread.currentThread();
- final ClassLoader originalLoader = currentThread.getContextClassLoader();
- try {
- currentThread.setContextClassLoader(errorThrowingLoader);
-
- // The rendering should succeed without propagating NoClassDefFoundError
- assertThatNoException().isThrownBy(() -> patternFormatter.format(logEvent, buffer));
- } finally {
- currentThread.setContextClassLoader(originalLoader);
- }
-
- // The output should still contain the exception and stack trace
- final String output = buffer.toString();
- assertThat(output).contains("test with custom classloader");
- assertThat(output).contains("com.example.UnloadedClass");
- }
-}
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 ea9294e58a4..f938c26e9e4 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
@@ -19,6 +19,7 @@
import static java.util.Arrays.asList;
import static org.apache.logging.log4j.util.Strings.LINE_SEPARATOR;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
import foo.TestFriendlyException;
import java.io.ByteArrayOutputStream;
@@ -384,6 +385,101 @@ void depth_and_package_limited_output_should_match_2(final DepthTestCase depthTe
" ... 3 more",
"Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]"));
}
+
+ /**
+ * Verifies that the extended stack trace renderer does not propagate
+ * {@link Throwable} when a class in the stack trace cannot be resolved.
+ *
+ * @see #4028
+ */
+ @Test
+ @Issue("https://github.com/apache/logging-log4j2/issues/4028")
+ void rendering_should_succeed_with_nonexistent_class_in_stack_trace() {
+
+ // Create an exception with a stack trace element referencing a non-existent class
+ final Exception exception = new Exception("test exception");
+ final StackTraceElement[] originalTrace = exception.getStackTrace();
+ final StackTraceElement[] modifiedTrace = new StackTraceElement[originalTrace.length + 1];
+ modifiedTrace[0] = new StackTraceElement(
+ "com.nonexistent.deliberately.missing.ClassName",
+ "someMethod",
+ "ClassName.java",
+ 42);
+ System.arraycopy(originalTrace, 0, modifiedTrace, 1, originalTrace.length);
+ exception.setStackTrace(modifiedTrace);
+
+ // Create the formatter using patternPrefix (e.g., "%xEx" for extended)
+ final List patternFormatters =
+ PATTERN_PARSER.parse(patternPrefix, false, true, true);
+ assertThat(patternFormatters).hasSize(1);
+ final PatternFormatter patternFormatter = patternFormatters.get(0);
+
+ final LogEvent logEvent = Log4jLogEvent.newBuilder()
+ .setThrown(exception)
+ .setLevel(LEVEL)
+ .build();
+ final StringBuilder buffer = new StringBuilder();
+
+ // The rendering should not throw any exception
+ assertThatNoException().isThrownBy(() -> patternFormatter.format(logEvent, buffer));
+
+ // The output should contain our non-existent class name and the exception message
+ final String output = buffer.toString();
+ assertThat(output).contains("com.nonexistent.deliberately.missing.ClassName");
+ assertThat(output).contains("test exception");
+ }
+
+ /**
+ * Verifies that the stack trace renderer gracefully handles a class loader
+ * that throws {@link NoClassDefFoundError} during class resolution.
+ *
+ * @see #4028
+ */
+ @Test
+ @Issue("https://github.com/apache/logging-log4j2/issues/4028")
+ void rendering_should_succeed_when_classloader_throws_linkage_error() {
+
+ // Create a class loader that always throws NoClassDefFoundError
+ final ClassLoader errorThrowingLoader = new ClassLoader(null) {
+ @Override
+ public Class> loadClass(final String name) throws ClassNotFoundException {
+ throw new NoClassDefFoundError("Simulated missing class: " + name);
+ }
+ };
+
+ final Exception exception = new Exception("test with custom classloader");
+ exception.setStackTrace(new StackTraceElement[] {
+ new StackTraceElement(
+ "com.example.UnloadedClass", "doWork", "UnloadedClass.java", 10),
+ new StackTraceElement(
+ "com.example.CallerClass", "invoke", "CallerClass.java", 20)
+ });
+
+ final List patternFormatters =
+ PATTERN_PARSER.parse(patternPrefix, false, true, true);
+ assertThat(patternFormatters).hasSize(1);
+ final PatternFormatter patternFormatter = patternFormatters.get(0);
+
+ final LogEvent logEvent = Log4jLogEvent.newBuilder()
+ .setThrown(exception)
+ .setLevel(LEVEL)
+ .build();
+ final StringBuilder buffer = new StringBuilder();
+
+ // Set the error-throwing class loader as the context class loader
+ final Thread currentThread = Thread.currentThread();
+ final ClassLoader originalLoader = currentThread.getContextClassLoader();
+ try {
+ currentThread.setContextClassLoader(errorThrowingLoader);
+ assertThatNoException().isThrownBy(() -> patternFormatter.format(logEvent, buffer));
+ } finally {
+ currentThread.setContextClassLoader(originalLoader);
+ }
+
+ final String output = buffer.toString();
+ assertThat(output).contains("test with custom classloader");
+ assertThat(output).contains("com.example.UnloadedClass");
+ }
}
abstract static class AbstractStackTraceTest {
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 752cc772b9a..2c7ea334bb1 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
@@ -181,7 +181,7 @@ private static Class> loadClass(final ClassLoader loader, final String classNa
if (clazz != null) {
return clazz;
}
- } catch (final Exception | LinkageError ignored) {
+ } catch (final Throwable ignored) {
// Do nothing
}
}
diff --git a/src/changelog/.2.x.x/4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml b/src/changelog/.2.x.x/4049_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
similarity index 53%
rename from src/changelog/.2.x.x/4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
rename to src/changelog/.2.x.x/4049_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
index 6ec8de1421b..f4828edda5e 100644
--- a/src/changelog/.2.x.x/4028_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
+++ b/src/changelog/.2.x.x/4049_fix_catch_LinkageError_in_ThrowableExtendedStackTraceRenderer.xml
@@ -4,6 +4,6 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
-
- Catch `LinkageError` in `ThrowableExtendedStackTraceRenderer#loadClass` to prevent `NoClassDefFoundError` from breaking logging
+
+ Ensure all `Throwable`s are handled while rendering stack traces