From b928e52473c2eaef02949aec5c429effd27c72b8 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 25 Jul 2026 07:32:31 -0400 Subject: [PATCH 1/5] fix: buffer and close streams in LoggerContextAdmin.setConfigLocationUri ConfigurationSource(InputStream, File/URL) leaves stream ownership with the caller. Buffer configuration bytes into a Source-backed ConfigurationSource and close the FileInputStream/URL stream before ConfigurationFactory runs so failed or successful JMX reconfigure paths do not leak descriptors. Signed-off-by: Sebastien Tardif --- ...rContextAdminSetConfigLocationUriTest.java | 81 +++++++++++++++++++ .../log4j/core/jmx/LoggerContextAdmin.java | 30 ++++++- ...ogger_context_admin_config_stream_leak.xml | 8 ++ 3 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java create mode 100644 src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java new file mode 100644 index 00000000000..7cf9456ce03 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java @@ -0,0 +1,81 @@ +/* + * 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.jmx; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.logging.log4j.core.LoggerContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Regression for stream ownership in {@link LoggerContextAdmin#setConfigLocationUri(String)}. + * The method buffers configuration bytes and closes the underlying File/URL stream before + * ConfigurationFactory runs. + */ +class LoggerContextAdminSetConfigLocationUriTest { + + @TempDir + Path tempDir; + + @Test + void setConfigLocationUri_loadsValidFileAndReconfigures() throws Exception { + final Path config = tempDir.resolve("log4j2-test.xml"); + writeConfig(config, "C"); + + final LoggerContext ctx = new LoggerContext("jmx-admin-stream-test"); + final LoggerContextAdmin admin = new LoggerContextAdmin(ctx, Runnable::run); + + assertDoesNotThrow( + () -> admin.setConfigLocationUri(config.toAbsolutePath().toString())); + assertTrue(ctx.getConfiguration().getAppenders().containsKey("C")); + } + + @Test + void setConfigLocationUri_fileUrlFormAlsoLoads() throws Exception { + final Path config = tempDir.resolve("log4j2-url.xml"); + writeConfig(config, "FromUrl"); + + final LoggerContext ctx = new LoggerContext("jmx-admin-file-url-test"); + final LoggerContextAdmin admin = new LoggerContextAdmin(ctx, Runnable::run); + final String fileUrl = config.toUri().toURL().toString(); + assertDoesNotThrow(() -> admin.setConfigLocationUri(fileUrl)); + assertTrue(ctx.getConfiguration().getAppenders().containsKey("FromUrl")); + } + + @Test + void setConfigLocationUri_rejectsBlankLocation() { + final LoggerContext ctx = new LoggerContext("jmx-admin-blank"); + final LoggerContextAdmin admin = new LoggerContextAdmin(ctx, Runnable::run); + assertThrows(IllegalArgumentException.class, () -> admin.setConfigLocationUri("")); + assertThrows(IllegalArgumentException.class, () -> admin.setConfigLocationUri(null)); + } + + private static void writeConfig(final Path config, final String appenderName) throws Exception { + final String xml = "\n" + + "\n" + + " \n" + + " \n" + + "\n"; + Files.write(config, xml.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java index d8927d6737d..3152f99e8cc 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java @@ -19,6 +19,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -45,6 +46,7 @@ import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; import org.apache.logging.log4j.core.util.Closer; +import org.apache.logging.log4j.core.util.Source; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.Strings; @@ -128,14 +130,26 @@ public void setConfigLocationUri(final String configLocation) throws URISyntaxEx LOGGER.debug("---------"); LOGGER.debug("Remote request to reconfigure using location " + configLocation); final File configFile = new File(configLocation); - ConfigurationSource configSource = null; + final ConfigurationSource configSource; + // Read the configuration into memory and close the underlying stream before handing + // off to ConfigurationFactory. ConfigurationSource(InputStream, File/URL) leaves stream + // ownership with the caller; without buffering, failures during getConfiguration/start + // can leave FileInputStream/URLConnection streams open. if (configFile.exists()) { LOGGER.debug("Opening config file {}", configFile.getAbsolutePath()); - configSource = new ConfigurationSource(new FileInputStream(configFile), configFile); + final byte[] data; + try (final InputStream in = new FileInputStream(configFile)) { + data = toByteArray(in); + } + configSource = new ConfigurationSource(new Source(configFile), data, configFile.lastModified()); } else { final URL configURL = new URL(configLocation); LOGGER.debug("Opening config URL {}", configURL); - configSource = new ConfigurationSource(configURL.openStream(), configURL); + final byte[] data; + try (final InputStream in = configURL.openStream()) { + data = toByteArray(in); + } + configSource = new ConfigurationSource(new Source(configURL), data, 0L); } final Configuration config = ConfigurationFactory.getInstance().getConfiguration(loggerContext, configSource); loggerContext.start(config); @@ -253,4 +267,14 @@ private long nextSeqNo() { private long now() { return System.currentTimeMillis(); } + + private static byte[] toByteArray(final InputStream inputStream) throws IOException { + final ByteArrayOutputStream contents = new ByteArrayOutputStream(Math.max(4096, inputStream.available())); + final byte[] buff = new byte[4096]; + int length; + while ((length = inputStream.read(buff)) >= 0) { + contents.write(buff, 0, length); + } + return contents.toByteArray(); + } } diff --git a/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml new file mode 100644 index 00000000000..577dbadf0b6 --- /dev/null +++ b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml @@ -0,0 +1,8 @@ + + + + Close configuration streams in JMX `LoggerContextAdmin.setConfigLocationUri` after buffering into `ConfigurationSource` + From 023f67044f568c4fe8f57bbefb20cea372327206 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 25 Jul 2026 07:33:59 -0400 Subject: [PATCH 2/5] changelog: set issue id to PR #4218 Signed-off-by: Sebastien Tardif --- .../.2.x.x/fix_logger_context_admin_config_stream_leak.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml index 577dbadf0b6..32065280d59 100644 --- a/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml +++ b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml @@ -3,6 +3,6 @@ xmlns="https://logging.apache.org/xml/ns" xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" type="fixed"> - + Close configuration streams in JMX `LoggerContextAdmin.setConfigLocationUri` after buffering into `ConfigurationSource` From 7ea5d6bf147e603915952396982a7706a299d18e Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sun, 2 Aug 2026 20:42:37 -0400 Subject: [PATCH 3/5] fix: keep stream-backed source; try-with-resources around load Address review on #4218: - Drop full buffering so ConfigurationSource remains stream/file backed (monitorInterval / resetInputStream still re-read the file). - Close the caller-owned InputStream with try-with-resources around getConfiguration and start. - Add a regression test that installs a factory which never consumes the stream; red fails on open FD count without the try-with-resources, green closes cleanly. Signed-off-by: Sebastien Tardif --- ...rContextAdminSetConfigLocationUriTest.java | 72 ++++++++++++++++++- .../log4j/core/jmx/LoggerContextAdmin.java | 38 ++++------ ...ogger_context_admin_config_stream_leak.xml | 2 +- 3 files changed, 83 insertions(+), 29 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java index 7cf9456ce03..0e521a678d8 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java @@ -17,26 +17,46 @@ package org.apache.logging.log4j.core.jmx; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import com.sun.management.UnixOperatingSystemMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.DefaultConfiguration; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; /** - * Regression for stream ownership in {@link LoggerContextAdmin#setConfigLocationUri(String)}. - * The method buffers configuration bytes and closes the underlying File/URL stream before - * ConfigurationFactory runs. + * Regression for caller-owned stream cleanup in + * {@link LoggerContextAdmin#setConfigLocationUri(String)}. + * + *

{@code ConfigurationSource(InputStream, File/URL)} leaves stream ownership + * with the caller. Built-in factories ({@code XmlConfiguration}, + * {@code JsonConfiguration}, {@code PropertiesConfigurationFactory}) close + * {@code getInputStream()} when they consume it, but that is not a substitute + * for caller-side try-with-resources when a factory path never reads the stream. */ class LoggerContextAdminSetConfigLocationUriTest { @TempDir Path tempDir; + @AfterEach + void resetConfigurationFactory() { + ConfigurationFactory.resetConfigurationFactory(); + } + @Test void setConfigLocationUri_loadsValidFileAndReconfigures() throws Exception { final Path config = tempDir.resolve("log4j2-test.xml"); @@ -70,6 +90,44 @@ void setConfigLocationUri_rejectsBlankLocation() { assertThrows(IllegalArgumentException.class, () -> admin.setConfigLocationUri(null)); } + /** + * Fails without the try-with-resources around {@code getConfiguration}/{@code start}: + * a factory that never consumes the stream leaves the {@code FileInputStream} open. + * With the fix, the caller always closes it. + */ + @Test + void setConfigLocationUri_closesCallerOwnedStreamWhenFactoryDoesNotConsumeIt() throws Exception { + final Path config = tempDir.resolve("log4j2-unconsumed.xml"); + writeConfig(config, "Unconsumed"); + + ConfigurationFactory.setConfigurationFactory(new ConfigurationFactory() { + @Override + public Configuration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) { + // Intentionally leave source.getInputStream() unconsumed and unclosed. + return new DefaultConfiguration(); + } + + @Override + protected String[] getSupportedTypes() { + return new String[] {"*"}; + } + }); + + final long expectedFdCount = getOpenFileDescriptorCount(); + final LoggerContext ctx = new LoggerContext("jmx-admin-stream-close"); + final LoggerContextAdmin admin = new LoggerContextAdmin(ctx, Runnable::run); + admin.setConfigLocationUri(config.toAbsolutePath().toString()); + + // UNIX: unclosed FileInputStream would leave an extra descriptor. + assertEquals(expectedFdCount, getOpenFileDescriptorCount()); + // Windows: an open FileInputStream locks the file against delete. + try { + Files.delete(config); + } catch (final Exception e) { + fail(e); + } + } + private static void writeConfig(final Path config, final String appenderName) throws Exception { final String xml = "\n" + "\n" @@ -78,4 +136,12 @@ private static void writeConfig(final Path config, final String appenderName) th + "\n"; Files.write(config, xml.getBytes(StandardCharsets.UTF_8)); } + + private static long getOpenFileDescriptorCount() { + final OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); + if (os instanceof UnixOperatingSystemMXBean) { + return ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount(); + } + return 0; + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java index 3152f99e8cc..f66aa8d85d6 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java @@ -19,7 +19,6 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -46,7 +45,6 @@ import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; import org.apache.logging.log4j.core.util.Closer; -import org.apache.logging.log4j.core.util.Source; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.Strings; @@ -130,29 +128,29 @@ public void setConfigLocationUri(final String configLocation) throws URISyntaxEx LOGGER.debug("---------"); LOGGER.debug("Remote request to reconfigure using location " + configLocation); final File configFile = new File(configLocation); - final ConfigurationSource configSource; - // Read the configuration into memory and close the underlying stream before handing - // off to ConfigurationFactory. ConfigurationSource(InputStream, File/URL) leaves stream - // ownership with the caller; without buffering, failures during getConfiguration/start - // can leave FileInputStream/URLConnection streams open. + // ConfigurationSource(InputStream, File/URL) documents that the caller owns the stream. + // XmlConfiguration, JsonConfiguration, and PropertiesConfigurationFactory close + // getInputStream() when they consume it, but that is factory-side cleanup. Keep a + // stream-backed source (so resetInputStream/reconfigure still re-reads the file for + // monitorInterval) and always close the caller-owned stream via try-with-resources. if (configFile.exists()) { LOGGER.debug("Opening config file {}", configFile.getAbsolutePath()); - final byte[] data; try (final InputStream in = new FileInputStream(configFile)) { - data = toByteArray(in); + final ConfigurationSource configSource = new ConfigurationSource(in, configFile); + final Configuration config = + ConfigurationFactory.getInstance().getConfiguration(loggerContext, configSource); + loggerContext.start(config); } - configSource = new ConfigurationSource(new Source(configFile), data, configFile.lastModified()); } else { final URL configURL = new URL(configLocation); LOGGER.debug("Opening config URL {}", configURL); - final byte[] data; try (final InputStream in = configURL.openStream()) { - data = toByteArray(in); + final ConfigurationSource configSource = new ConfigurationSource(in, configURL); + final Configuration config = + ConfigurationFactory.getInstance().getConfiguration(loggerContext, configSource); + loggerContext.start(config); } - configSource = new ConfigurationSource(new Source(configURL), data, 0L); } - final Configuration config = ConfigurationFactory.getInstance().getConfiguration(loggerContext, configSource); - loggerContext.start(config); LOGGER.debug("Completed remote request to reconfigure."); } @@ -267,14 +265,4 @@ private long nextSeqNo() { private long now() { return System.currentTimeMillis(); } - - private static byte[] toByteArray(final InputStream inputStream) throws IOException { - final ByteArrayOutputStream contents = new ByteArrayOutputStream(Math.max(4096, inputStream.available())); - final byte[] buff = new byte[4096]; - int length; - while ((length = inputStream.read(buff)) >= 0) { - contents.write(buff, 0, length); - } - return contents.toByteArray(); - } } diff --git a/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml index 32065280d59..b1ed996a075 100644 --- a/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml +++ b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml @@ -4,5 +4,5 @@ xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" type="fixed"> - Close configuration streams in JMX `LoggerContextAdmin.setConfigLocationUri` after buffering into `ConfigurationSource` + Close caller-owned configuration streams in JMX `LoggerContextAdmin.setConfigLocationUri` with try-with-resources (keep stream-backed `ConfigurationSource` for file watchers) From b323bff7a99e1d70743b07a9d24382ca663c812e Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 13 Aug 2026 05:28:47 -0700 Subject: [PATCH 4/5] test: portable closed-stream check for JMX setConfigLocationUri Address vy review: drop OS FD-count/delete assertions in favor of an AtomicReference + ConfigurationFactory capture of source.getInputStream(), then assertThrows(IOException) on read after setConfigLocationUri. Also switch the file path to Files.newInputStream as requested. --- ...rContextAdminSetConfigLocationUriTest.java | 47 +++++++------------ .../log4j/core/jmx/LoggerContextAdmin.java | 4 +- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java index 0e521a678d8..01fb5811d1f 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java @@ -17,17 +17,16 @@ package org.apache.logging.log4j.core.jmx; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import com.sun.management.UnixOperatingSystemMXBean; -import java.lang.management.ManagementFactory; -import java.lang.management.OperatingSystemMXBean; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; @@ -42,10 +41,9 @@ * {@link LoggerContextAdmin#setConfigLocationUri(String)}. * *

{@code ConfigurationSource(InputStream, File/URL)} leaves stream ownership - * with the caller. Built-in factories ({@code XmlConfiguration}, - * {@code JsonConfiguration}, {@code PropertiesConfigurationFactory}) close - * {@code getInputStream()} when they consume it, but that is not a substitute - * for caller-side try-with-resources when a factory path never reads the stream. + * with the caller. Built-in factories close {@code getInputStream()} when they + * consume it, but that is not a substitute for caller-side try-with-resources + * when a factory path never reads the stream. */ class LoggerContextAdminSetConfigLocationUriTest { @@ -91,19 +89,21 @@ void setConfigLocationUri_rejectsBlankLocation() { } /** - * Fails without the try-with-resources around {@code getConfiguration}/{@code start}: - * a factory that never consumes the stream leaves the {@code FileInputStream} open. - * With the fix, the caller always closes it. + * Portable red-green for caller try-with-resources: install a factory that + * returns without consuming the source stream, capture that stream, and + * assert it is closed after {@code setConfigLocationUri} returns. */ @Test void setConfigLocationUri_closesCallerOwnedStreamWhenFactoryDoesNotConsumeIt() throws Exception { final Path config = tempDir.resolve("log4j2-unconsumed.xml"); writeConfig(config, "Unconsumed"); + final AtomicReference input = new AtomicReference<>(); ConfigurationFactory.setConfigurationFactory(new ConfigurationFactory() { @Override public Configuration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) { - // Intentionally leave source.getInputStream() unconsumed and unclosed. + // Capture the caller-owned stream without consuming or closing it. + input.set(source.getInputStream()); return new DefaultConfiguration(); } @@ -113,19 +113,14 @@ protected String[] getSupportedTypes() { } }); - final long expectedFdCount = getOpenFileDescriptorCount(); final LoggerContext ctx = new LoggerContext("jmx-admin-stream-close"); final LoggerContextAdmin admin = new LoggerContextAdmin(ctx, Runnable::run); admin.setConfigLocationUri(config.toAbsolutePath().toString()); - // UNIX: unclosed FileInputStream would leave an extra descriptor. - assertEquals(expectedFdCount, getOpenFileDescriptorCount()); - // Windows: an open FileInputStream locks the file against delete. - try { - Files.delete(config); - } catch (final Exception e) { - fail(e); - } + final InputStream captured = input.get(); + assertNotNull(captured); + // Closed streams throw on read; an unclosed stream would still read. + assertThrows(IOException.class, captured::read); } private static void writeConfig(final Path config, final String appenderName) throws Exception { @@ -136,12 +131,4 @@ private static void writeConfig(final Path config, final String appenderName) th + "\n"; Files.write(config, xml.getBytes(StandardCharsets.UTF_8)); } - - private static long getOpenFileDescriptorCount() { - final OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); - if (os instanceof UnixOperatingSystemMXBean) { - return ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount(); - } - return 0; - } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java index f66aa8d85d6..10223843a80 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/LoggerContextAdmin.java @@ -20,7 +20,6 @@ import java.beans.PropertyChangeListener; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -31,6 +30,7 @@ import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; @@ -135,7 +135,7 @@ public void setConfigLocationUri(final String configLocation) throws URISyntaxEx // monitorInterval) and always close the caller-owned stream via try-with-resources. if (configFile.exists()) { LOGGER.debug("Opening config file {}", configFile.getAbsolutePath()); - try (final InputStream in = new FileInputStream(configFile)) { + try (final InputStream in = Files.newInputStream(configFile.toPath())) { final ConfigurationSource configSource = new ConfigurationSource(in, configFile); final Configuration config = ConfigurationFactory.getInstance().getConfiguration(loggerContext, configSource); From c986111e360de4eba3be24a259c8bdc6c8d7b157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?= Date: Fri, 14 Aug 2026 09:58:06 +0200 Subject: [PATCH 5/5] Simplify changelog --- .../.2.x.x/fix_logger_context_admin_config_stream_leak.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml index b1ed996a075..091e6a5087c 100644 --- a/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml +++ b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml @@ -4,5 +4,7 @@ xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" type="fixed"> - Close caller-owned configuration streams in JMX `LoggerContextAdmin.setConfigLocationUri` with try-with-resources (keep stream-backed `ConfigurationSource` for file watchers) + + Fix file descriptor leak in the JMX integration +