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