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]
