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

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 3260be4edf Review "Keep streams returned from ReferenceCountedJar open"
3260be4edf is described below

commit 3260be4edf2dbdfbdb49545aef967136b6c777bb
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Nov 4 15:08:06 2026 +0000

    Review "Keep streams returned from ReferenceCountedJar open"
    
    Expand Javadoc
    Fix leak in JspUtil
    
    Co-authored by: Codex / GPT-6.1-Sol
---
 java/org/apache/jasper/compiler/JspUtil.java       | 17 ++---
 .../tomcat/util/scan/ReferenceCountedJar.java      |  5 ++
 .../jasper/compiler/TestJspUtilGetReader.java      | 88 ++++++++++++++++++++++
 webapps/docs/changelog.xml                         |  5 ++
 4 files changed, 106 insertions(+), 9 deletions(-)

diff --git a/java/org/apache/jasper/compiler/JspUtil.java 
b/java/org/apache/jasper/compiler/JspUtil.java
index 4d29cfd483..ca61c9d3bb 100644
--- a/java/org/apache/jasper/compiler/JspUtil.java
+++ b/java/org/apache/jasper/compiler/JspUtil.java
@@ -937,18 +937,17 @@ public class JspUtil {
             for (int i = 0; i < skip; i++) {
                 in.read();
             }
-        } catch (IOException ioe) {
-            try {
-                in.close();
-            } catch (IOException ignore) {
-                // Ignore
-            }
-            throw ioe;
-        }
-        try {
             reader = new InputStreamReader(in, encoding);
         } catch (UnsupportedEncodingException ex) {
             err.jspError("jsp.error.unsupported.encoding", encoding);
+        } finally {
+            if (reader == null) {
+                try {
+                    in.close();
+                } catch (IOException ignore) {
+                    // Ignore
+                }
+            }
         }
 
         return reader;
diff --git a/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java 
b/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java
index 6330ae3c24..a1d4860607 100644
--- a/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java
+++ b/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java
@@ -27,6 +27,11 @@ import org.apache.tomcat.Jar;
 /**
  * This class provides a wrapper around {@link Jar} that uses reference 
counting to close and re-create the wrapped
  * {@link Jar} instance as required.
+ * <p>
+ * Each stream returned by {@link #getInputStream(String)} or {@link 
#getEntryInputStream()} holds a reference to the
+ * wrapped {@link Jar} until it is closed. The wrapped {@link Jar}, and its 
underlying file handle, therefore remains
+ * open until every returned stream has been closed, even if all other 
references have been released. Callers are
+ * responsible for closing every stream they obtain.
  */
 public class ReferenceCountedJar implements Jar {
 
diff --git a/test/org/apache/jasper/compiler/TestJspUtilGetReader.java 
b/test/org/apache/jasper/compiler/TestJspUtilGetReader.java
new file mode 100644
index 0000000000..eea69cbba6
--- /dev/null
+++ b/test/org/apache/jasper/compiler/TestJspUtilGetReader.java
@@ -0,0 +1,88 @@
+/*
+ * 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.jasper.compiler;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.apache.jasper.JasperException;
+import org.apache.tomcat.util.scan.ReferenceCountedJar;
+
+public class TestJspUtilGetReader {
+
+    private static final String TAG_NAME = "META-INF/tags/test.tag";
+
+    @Rule
+    public TemporaryFolder testDir = new TemporaryFolder();
+
+    @Test
+    public void testUnsupportedEncodingClosesJar() throws Exception {
+        ReferenceCountedJar jar = createJar();
+        Field wrappedJar = 
ReferenceCountedJar.class.getDeclaredField("wrappedJar");
+        wrappedJar.setAccessible(true);
+
+        try {
+            try (jar) {
+                JasperException exception = 
Assert.assertThrows(JasperException.class,
+                        () -> JspUtil.getReader('/' + TAG_NAME, 
"does-not-exist", jar, null, new ErrorDispatcher(false)));
+                Assert.assertTrue(exception.getMessage(), 
exception.getMessage().contains("does-not-exist"));
+            }
+
+            // The failed reader must release its stream reference so the 
owner's close can close the JAR.
+            Assert.assertNull(wrappedJar.get(jar));
+        } finally {
+            // Release a leaked stream reference if the assertion fails.
+            if (wrappedJar.get(jar) != null) {
+                jar.close();
+            }
+        }
+    }
+
+
+    @Test
+    public void testReaderRemainsOpenAfterJarClose() throws Exception {
+        ReferenceCountedJar jar = createJar();
+        InputStreamReader reader;
+        try (jar) {
+            reader = JspUtil.getReader('/' + TAG_NAME, "UTF-8", jar, null, new 
ErrorDispatcher(false));
+        }
+        try (reader) {
+            Assert.assertEquals('h', reader.read());
+        }
+    }
+
+
+    private ReferenceCountedJar createJar() throws Exception {
+        File file = testDir.newFile("tags.jar");
+        try (JarOutputStream jos = new JarOutputStream(new 
FileOutputStream(file))) {
+            jos.putNextEntry(new JarEntry(TAG_NAME));
+            jos.write("hello".getBytes(StandardCharsets.UTF_8));
+            jos.closeEntry();
+        }
+        return new ReferenceCountedJar(file.toURI().toURL());
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 6be68edd54..764f7a2762 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -854,6 +854,11 @@
         <code>BeanELResolver</code> from resolving bean-style properties (such
         as <code>isValid()</code>) on a <code>Record</code>. (markt)
       </fix>
+      <fix>
+        Fix a JAR file handle leak when a tag file specifies an unsupported
+        <code>pageEncoding</code>. Close the input stream if reader creation
+        fails so that its reference to the JAR is released. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Cluster">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to