This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a commit to branch fix/2.x/3819-logback-builder-reuse
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 05d97d2ac1ed51b071541d53b7e18b8852d4c18a
Author: Piotr P. Karwasz <pkarwasz-git...@apache.org>
AuthorDate: Mon Jul 14 13:21:24 2025 +0200
fix: Prevent `LogBuilder` memory leak in Log4j API to Logback bridge
This change fixes a potential memory leak in the `log4j-to-slf4j` module
(Log4j API to Logback bridge) caused by the use of a thread-local `LogBuilder`
pool.
The leak occurred because the thread-local field was accessed, even when
`log4j2.enableThreadlocals` was set to `false`. In servlet environments, this
could lead to classloader leaks due to the persistence of thread-local
references.
With this fix, all access to the `ThreadLocal` is now **properly** gated by
the `log4j2.enableThreadlocals` system property—matching how similar pooling
behavior is handled in Log4j Core.
This preserves the GC-free option for advanced users who explicitly opt in
via `log4j2.enableThreadlocals = true`, while avoiding memory leaks for others.
Fixes #3819
---
log4j-to-slf4j/pom.xml | 11 ++++++
.../java/org/apache/logging/slf4j/SLF4JLogger.java | 16 +++++---
.../{LoggerTest.java => SLF4JLoggerTest.java} | 43 +++++++++++++++++++++-
.../slf4j/{LoggerTest.xml => SLF4JLoggerTest.xml} | 0
.../.2.x.x/3819_logback-builder-reuse.xml | 12 ++++++
5 files changed, 75 insertions(+), 7 deletions(-)
diff --git a/log4j-to-slf4j/pom.xml b/log4j-to-slf4j/pom.xml
index 3a6b0d9e85..95e3ec5fc9 100644
--- a/log4j-to-slf4j/pom.xml
+++ b/log4j-to-slf4j/pom.xml
@@ -153,6 +153,17 @@
</execution>
</executions>
</plugin>
+
+ <!-- Illegal access is disabled by default in Java 16 due to JEP-396.
+ We are relaxing it for tests. -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
+ </configuration>
+ </plugin>
+
</plugins>
</build>
</project>
diff --git
a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
index 26e94c67b3..ff9410f33e 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
@@ -42,17 +42,21 @@ public class SLF4JLogger extends AbstractLogger {
private final org.slf4j.Logger logger;
private final LocationAwareLogger locationAwareLogger;
+ private final boolean useThreadLocal;
public SLF4JLogger(final String name, final MessageFactory messageFactory,
final org.slf4j.Logger logger) {
- super(name, messageFactory);
- this.logger = logger;
- this.locationAwareLogger = logger instanceof LocationAwareLogger ?
(LocationAwareLogger) logger : null;
+ this(name, messageFactory, logger, Constants.ENABLE_THREADLOCALS);
}
public SLF4JLogger(final String name, final org.slf4j.Logger logger) {
- super(name);
+ this(name, null, logger);
+ }
+
+ SLF4JLogger(String name, MessageFactory messageFactory, org.slf4j.Logger
logger, boolean useThreadLocal) {
+ super(name, messageFactory);
this.logger = logger;
this.locationAwareLogger = logger instanceof LocationAwareLogger ?
(LocationAwareLogger) logger : null;
+ this.useThreadLocal = useThreadLocal;
}
private int convertLevel(final Level level) {
@@ -364,8 +368,8 @@ public class SLF4JLogger extends AbstractLogger {
@Override
protected LogBuilder getLogBuilder(final Level level) {
- final SLF4JLogBuilder builder = logBuilder.get();
- return Constants.ENABLE_THREADLOCALS && !builder.isInUse()
+ SLF4JLogBuilder builder;
+ return useThreadLocal && !(builder = logBuilder.get()).isInUse()
? builder.reset(this, level)
: new SLF4JLogBuilder(this, level);
}
diff --git
a/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java
b/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/SLF4JLoggerTest.java
similarity index 82%
rename from
log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java
rename to
log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/SLF4JLoggerTest.java
index 6c378ad958..9e3a5856a0 100644
--- a/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java
+++ b/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/SLF4JLoggerTest.java
@@ -32,10 +32,13 @@ import static org.junit.jupiter.api.Assertions.fail;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.testUtil.StringListAppender;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
import java.util.List;
+import org.apache.logging.log4j.LogBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
@@ -45,13 +48,17 @@ import
org.apache.logging.log4j.message.StringFormatterMessageFactory;
import org.apache.logging.log4j.spi.AbstractLogger;
import org.apache.logging.log4j.spi.MessageFactory2Adapter;
import org.apache.logging.log4j.test.junit.UsingStatusListener;
+import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.junitpioneer.jupiter.Issue;
import org.slf4j.MDC;
@UsingStatusListener
@LoggerContextSource
-class LoggerTest {
+class SLF4JLoggerTest {
private static final Object OBJ = new Object();
// Log4j objects
@@ -265,4 +272,38 @@ class LoggerTest {
// expected
}
}
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ @Issue("https://github.com/apache/logging-log4j2/issues/3819")
+ void threadLocalUsage(boolean useThreadLocal) throws
ReflectiveOperationException {
+ // Reset the static ThreadLocal in SLF4JLogger
+ getLogBuilderThreadLocal().remove();
+ final org.slf4j.Logger slf4jLogger = context.getLogger(getClass());
+ Logger logger = new SLF4JLogger(slf4jLogger.getName(), null,
slf4jLogger, useThreadLocal);
+ LogBuilder builder1 = logger.atInfo();
+ builder1.log("Test message");
+ LogBuilder builder2 = logger.atInfo();
+ builder2.log("Another test message");
+ // Check if the same builder is reused based on the useThreadLocal flag
+ Assertions.assertThat(isThreadLocalPresent())
+ .as("ThreadLocal should be present iff useThreadLocal is
enabled")
+ .isEqualTo(useThreadLocal);
+ Assertions.assertThat(builder2 == builder1)
+ .as("Builder2 should be the same as Builder1 iff
useThreadLocal is enabled")
+ .isEqualTo(useThreadLocal);
+ }
+
+ private static boolean isThreadLocalPresent() throws
ReflectiveOperationException {
+ Method isPresentMethod =
ThreadLocal.class.getDeclaredMethod("isPresent");
+ isPresentMethod.setAccessible(true);
+ ThreadLocal<?> threadLocal = getLogBuilderThreadLocal();
+ return (boolean) isPresentMethod.invoke(threadLocal);
+ }
+
+ private static ThreadLocal<?> getLogBuilderThreadLocal() throws
ReflectiveOperationException {
+ Field logBuilderField =
SLF4JLogger.class.getDeclaredField("logBuilder");
+ logBuilderField.setAccessible(true);
+ return (ThreadLocal<?>) logBuilderField.get(null);
+ }
}
diff --git
a/log4j-to-slf4j/src/test/resources/org/apache/logging/slf4j/LoggerTest.xml
b/log4j-to-slf4j/src/test/resources/org/apache/logging/slf4j/SLF4JLoggerTest.xml
similarity index 100%
rename from
log4j-to-slf4j/src/test/resources/org/apache/logging/slf4j/LoggerTest.xml
rename to
log4j-to-slf4j/src/test/resources/org/apache/logging/slf4j/SLF4JLoggerTest.xml
diff --git a/src/changelog/.2.x.x/3819_logback-builder-reuse.xml
b/src/changelog/.2.x.x/3819_logback-builder-reuse.xml
new file mode 100644
index 0000000000..ea67aa50d0
--- /dev/null
+++ b/src/changelog/.2.x.x/3819_logback-builder-reuse.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="3819"
link="https://github.com/apache/logging-log4j2/issues/3819"/>
+ <description format="asciidoc">
+ Fix potential memory leak involving `LogBuilder` in Log4j API to Logback
bridge.
+ </description>
+</entry>