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

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 7c323e62ce50edd7f71285108bda669dbe9d7664
Author: opencode <[email protected]>
AuthorDate: Mon Oct 19 09:51:15 2026 +0200

    Avoid NullPointerException when a cached TLD is removed
    
    TldCache entries are populated at startup. If a TLD file inside the web
    application is removed while the context is running, the next JSP
    compilation referencing the tag library checks the last modified time
    of the cached entry. ServletContext.getResource() returns null for a
    removed resource and that null was passed straight to the
    CloseableURLConnection constructor, which dereferences it. The
    resulting NullPointerException was not caught by the surrounding
    IOException handler so compiling a referencing page failed with a raw
    NPE/500.
    
    Treat a missing resource as an unknown last modified time, matching the
    -1 values the jar based implementations already use for removed
    entries. The cached entry is then seen as out of date and the re-parse
    of the removed TLD reports the failure via a JasperException wrapping
    the IOException from opening the TLD rather than serving stale data.
    If the TLD is restored, the recomputed last modified time differs from
    the stored -1 values and the TLD is re-parsed and cached as usual.
    
    Found by code review.
---
 java/org/apache/jasper/compiler/TldCache.java     |  6 +++
 test/org/apache/jasper/compiler/TestTldCache.java | 49 +++++++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/java/org/apache/jasper/compiler/TldCache.java 
b/java/org/apache/jasper/compiler/TldCache.java
index c7109c68ed..1cf54cca7a 100644
--- a/java/org/apache/jasper/compiler/TldCache.java
+++ b/java/org/apache/jasper/compiler/TldCache.java
@@ -175,6 +175,12 @@ public class TldCache {
                 // webappPath will be null for JARs containing TLDs that are on
                 // the class path but not part of the web application
                 URL url = 
servletContext.getResource(tldResourcePath.getWebappPath());
+                if (url == null) {
+                    // The resource has been removed. Return -1 values so that
+                    // the cached entry is seen as out of date and re-parsing
+                    // reports the failure rather than using stale data
+                    return result;
+                }
                 try (CloseableURLConnection conn = new 
CloseableURLConnection(url)) {
                     result[0] = conn.getLastModified();
                 }
diff --git a/test/org/apache/jasper/compiler/TestTldCache.java 
b/test/org/apache/jasper/compiler/TestTldCache.java
index 9c2c56dc68..803747336c 100644
--- a/test/org/apache/jasper/compiler/TestTldCache.java
+++ b/test/org/apache/jasper/compiler/TestTldCache.java
@@ -17,6 +17,7 @@
 package org.apache.jasper.compiler;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.net.URL;
@@ -29,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.junit.Assert;
 import org.junit.Test;
 
+import org.apache.jasper.JasperException;
 import org.apache.tomcat.unittest.TesterServletContext;
 import org.apache.tomcat.util.descriptor.tld.TaglibXml;
 import org.apache.tomcat.util.descriptor.tld.TldParser;
@@ -41,6 +43,7 @@ public class TestTldCache {
 
     private static final String URI_1 = "http://tomcat.apache.org/TesterTld1";;
     private static final String URI_2 = "http://tomcat.apache.org/TesterTld2";;
+    private static final String URI_3 = "http://tomcat.apache.org/TesterTld3";;
 
 
     private static class TesterTldCacheServletContext extends 
TesterServletContext {
@@ -157,6 +160,52 @@ public class TestTldCache {
     }
 
 
+    /*
+     * A TLD that was present at startup is cached. If the TLD file is
+     * subsequently removed, ServletContext.getResource() returns null for it.
+     * getLastModified() previously passed that null on to
+     * CloseableURLConnection which threw a NullPointerException.
+     */
+    @Test
+    public void testRemovedTld() throws Exception {
+        File tldFile = createTld("test-tld-cache-removed", URI_3);
+        URL url = tldFile.toURI().toURL();
+
+        TesterTldCacheServletContext context = new 
TesterTldCacheServletContext();
+        context.resources.put("/removed.tld", url);
+
+        TldParser parser = new TldParser(true, false, true);
+        TldResourcePath path = new TldResourcePath(url, "/removed.tld");
+
+        Map<String,TldResourcePath> uriMap = new HashMap<>();
+        uriMap.put(URI_3, path);
+
+        Map<TldResourcePath,TaglibXml> xmlMap = new HashMap<>();
+        xmlMap.put(path, parser.parse(path));
+
+        TldCache cache = new TldCache(context, uriMap, xmlMap);
+
+        // The cached entry is up to date
+        Assert.assertEquals(URI_3, cache.getTaglibXml(path).getUri());
+
+        // Remove the TLD. The context, like a real web application context,
+        // no longer finds a resource at the path
+        context.resources.remove("/removed.tld");
+        Assert.assertTrue(tldFile.delete());
+
+        // The cached entry is out of date and the re-parse of the removed TLD
+        // must fail with a JasperException rather than a NullPointerException
+        try {
+            cache.getTaglibXml(path);
+            Assert.fail("Expected a JasperException for the removed TLD");
+        } catch (JasperException e) {
+            // Expected. The re-parse fails because the TLD can no longer be
+            // opened
+            Assert.assertTrue(String.valueOf(e.getCause()), e.getCause() 
instanceof FileNotFoundException);
+        }
+    }
+
+
     private void hammer(TldCache cache, TldResourcePath path, String 
expectedUri,
             CountDownLatch start, AtomicBoolean running,
             AtomicReference<Throwable> error) {


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

Reply via email to