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

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

commit c600bfd017f70e3dc0d05d669c4d99caeee93345
Author: opencode <[email protected]>
AuthorDate: Mon Oct 12 20:52:04 2026 +0200

    Never return null from StandardRoot resource lookups while stopping
    
    StandardRoot.stopInternal() removes the main resources from the sets
    that are queried before it moves the state to STOPPING. Since the state
    is available throughout that window, requests that are still in progress
    are not rejected but they no longer see the main resources, so
    getResourceInternal() fell through to returning the value of mainEmpty,
    which was never set.
    
    That null contradicts the documented contract of the resource lookup
    methods and left the callers that rely on it exposed to a null pointer
    exception, for example WebappClassLoaderBase.findClassInternal() and
    CachedResource.validateResource() both call exists() on the result
    without checking it. Return a non-existent resource instead, as the
    other resource sets do for a path they do not contain, and drop the
    documentation of the null return that was added earlier.
    
    Add a unit test that keeps the stop window open with a tracked resource
    which blocks while it is being closed, and checks that a lookup during
    that window still returns a non-existent resource.
---
 .../apache/catalina/webresources/StandardRoot.java | 10 ++-
 .../catalina/webresources/TestStandardRoot.java    | 77 +++++++++++++++++++++-
 2 files changed, 84 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/catalina/webresources/StandardRoot.java 
b/java/org/apache/catalina/webresources/StandardRoot.java
index 3cf39114ff..c17b91bece 100644
--- a/java/org/apache/catalina/webresources/StandardRoot.java
+++ b/java/org/apache/catalina/webresources/StandardRoot.java
@@ -218,8 +218,7 @@ public class StandardRoot extends LifecycleMBeanBase 
implements WebResourceRoot
      * @param path The path of the resource to retrieve
      * @param validate Whether to validate and normalize the path
      * @param useClassLoaderResources Whether to search class loader resources
-     * @return The web resource, or a non-existent resource if not found. May 
return {@code null} if the resource sets
-     *             are being removed concurrently while this method is 
executing (during shutdown)
+     * @return The web resource, or a non-existent resource if not found
      */
     protected WebResource getResource(String path, boolean validate, boolean 
useClassLoaderResources) {
         if (validate) {
@@ -316,6 +315,13 @@ public class StandardRoot extends LifecycleMBeanBase 
implements WebResourceRoot
         }
 
         // Default is empty resource in main resources
+        if (mainEmpty == null) {
+            // The main resources are removed from allResources while the root
+            // is stopping, but requests that are still in progress during that
+            // window need a non-existent resource rather than null.
+            return new EmptyResource(this, path);
+        }
+
         return mainEmpty;
     }
 
diff --git a/test/org/apache/catalina/webresources/TestStandardRoot.java 
b/test/org/apache/catalina/webresources/TestStandardRoot.java
index 2a66c33c5f..f5136d417c 100644
--- a/test/org/apache/catalina/webresources/TestStandardRoot.java
+++ b/test/org/apache/catalina/webresources/TestStandardRoot.java
@@ -20,14 +20,22 @@ import java.io.File;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 import org.junit.Assert;
 import org.junit.Test;
 
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.TrackedWebResource;
+import org.apache.catalina.WebResource;
 import org.apache.catalina.WebResourceRoot;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
 import org.apache.catalina.webresources.StandardRoot.BaseLocation;
 
-public class TestStandardRoot {
+public class TestStandardRoot extends TomcatBaseTest {
 
     private static final File file;
     private static final String fileUrl;
@@ -87,4 +95,71 @@ public class TestStandardRoot {
         WebResourceRoot root = new StandardRoot();
         root.setArchiveIndexStrategy("UNRECOGNIZED");
     }
+
+    @Test
+    public void testGetResourceWhileStopping() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+        File docBase = new File("test/webresources/dir1");
+        Context context = tomcat.addContext("", docBase.getAbsolutePath());
+        tomcat.start();
+
+        StandardRoot resourceRoot = (StandardRoot) context.getResources();
+        resourceRoot.setCachingAllowed(false);
+
+        // stopInternal() removes the main resources before the state moves to
+        // STOPPING, so requests that are in progress during that window do not
+        // see them. Such requests must still be given a non-existent resource
+        // rather than null. The window is held open by a tracked resource that
+        // blocks while stopInternal() closes it.
+        CountDownLatch closing = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+
+        resourceRoot.registerTrackedResource(new TrackedWebResource() {
+
+            @Override
+            public String getName() {
+                return "test";
+            }
+
+            @Override
+            public Exception getCreatedBy() {
+                return null;
+            }
+
+            @Override
+            public void close() {
+                closing.countDown();
+                try {
+                    release.await(30, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        });
+
+        Thread stopThread = new Thread(() -> {
+            try {
+                resourceRoot.stop();
+            } catch (LifecycleException e) {
+                // Not expected, the assertions below will fail
+            }
+        });
+        stopThread.start();
+
+        try {
+            Assert.assertTrue(closing.await(30, TimeUnit.SECONDS));
+
+            WebResource resource = resourceRoot.getResource("/missing.txt");
+            Assert.assertNotNull(resource);
+            Assert.assertFalse(resource.exists());
+        } finally {
+            // Never leave the stop thread blocked if an assertion fails
+            release.countDown();
+        }
+
+        stopThread.join(30000);
+
+        // Leave the resource root running for the standard tear-down
+        resourceRoot.start();
+    }
 }


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

Reply via email to