This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/2.x by this push:
     new ef6ebfbfc8 fix: close streams in 
`LoggerContextAdmin::setConfigLocationUri` (#4218)
ef6ebfbfc8 is described below

commit ef6ebfbfc80da589e2f1b4d1d20caa80d2514eb2
Author: Sebastien Tardif <[email protected]>
AuthorDate: Mon Aug 17 03:09:31 2026 -0700

    fix: close streams in `LoggerContextAdmin::setConfigLocationUri` (#4218)
    
    Signed-off-by: Sebastien Tardif <[email protected]>
    Co-authored-by: Ramanathan <[email protected]>
    Co-authored-by: Volkan Yazıcı <[email protected]>
---
 ...LoggerContextAdminSetConfigLocationUriTest.java | 134 +++++++++++++++++++++
 .../logging/log4j/core/jmx/LoggerContextAdmin.java |  24 +++-
 ...fix_logger_context_admin_config_stream_leak.xml |  10 ++
 3 files changed, 162 insertions(+), 6 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
new file mode 100644
index 0000000000..01fb5811d1
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/jmx/LoggerContextAdminSetConfigLocationUriTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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;
+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 caller-owned stream cleanup in
+ * {@link LoggerContextAdmin#setConfigLocationUri(String)}.
+ *
+ * <p>{@code ConfigurationSource(InputStream, File/URL)} leaves stream 
ownership
+ * 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 {
+
+    @TempDir
+    Path tempDir;
+
+    @AfterEach
+    void resetConfigurationFactory() {
+        ConfigurationFactory.resetConfigurationFactory();
+    }
+
+    @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));
+    }
+
+    /**
+     * 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<InputStream> input = new AtomicReference<>();
+        ConfigurationFactory.setConfigurationFactory(new 
ConfigurationFactory() {
+            @Override
+            public Configuration getConfiguration(final LoggerContext 
loggerContext, final ConfigurationSource source) {
+                // Capture the caller-owned stream without consuming or 
closing it.
+                input.set(source.getInputStream());
+                return new DefaultConfiguration();
+            }
+
+            @Override
+            protected String[] getSupportedTypes() {
+                return new String[] {"*"};
+            }
+        });
+
+        final LoggerContext ctx = new LoggerContext("jmx-admin-stream-close");
+        final LoggerContextAdmin admin = new LoggerContextAdmin(ctx, 
Runnable::run);
+        admin.setConfigLocationUri(config.toAbsolutePath().toString());
+
+        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 {
+        final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<Configuration status=\"OFF\">\n"
+                + "  <Appenders><Console name=\"" + appenderName + "\" 
target=\"SYSTEM_OUT\"/></Appenders>\n"
+                + "  <Loggers><Root level=\"error\"><AppenderRef ref=\"" + 
appenderName + "\"/></Root></Loggers>\n"
+                + "</Configuration>\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 d8927d6737..10223843a8 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.PropertyChangeEvent;
 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.URISyntaxException;
 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;
@@ -128,17 +128,29 @@ public class LoggerContextAdmin extends 
NotificationBroadcasterSupport
         LOGGER.debug("---------");
         LOGGER.debug("Remote request to reconfigure using location " + 
configLocation);
         final File configFile = new File(configLocation);
-        ConfigurationSource configSource = null;
+        // 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());
-            configSource = new ConfigurationSource(new 
FileInputStream(configFile), configFile);
+            try (final InputStream in = 
Files.newInputStream(configFile.toPath())) {
+                final ConfigurationSource configSource = new 
ConfigurationSource(in, configFile);
+                final Configuration config =
+                        
ConfigurationFactory.getInstance().getConfiguration(loggerContext, 
configSource);
+                loggerContext.start(config);
+            }
         } else {
             final URL configURL = new URL(configLocation);
             LOGGER.debug("Opening config URL {}", configURL);
-            configSource = new ConfigurationSource(configURL.openStream(), 
configURL);
+            try (final InputStream in = configURL.openStream()) {
+                final ConfigurationSource configSource = new 
ConfigurationSource(in, configURL);
+                final Configuration config =
+                        
ConfigurationFactory.getInstance().getConfiguration(loggerContext, 
configSource);
+                loggerContext.start(config);
+            }
         }
-        final Configuration config = 
ConfigurationFactory.getInstance().getConfiguration(loggerContext, 
configSource);
-        loggerContext.start(config);
         LOGGER.debug("Completed remote request to reconfigure.");
     }
 
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 0000000000..091e6a5087
--- /dev/null
+++ b/src/changelog/.2.x.x/fix_logger_context_admin_config_stream_leak.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       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">
+  <issue id="4218" link="https://github.com/apache/logging-log4j2/pull/4218"/>
+  <description format="asciidoc">
+    Fix file descriptor leak in the JMX integration
+  </description>
+</entry>

Reply via email to