This is an automated email from the ASF dual-hosted git repository.
dsoumis pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new e53d9b3cba Fix race condition in StandardContext.getServletContext()
e53d9b3cba is described below
commit e53d9b3cbac881b2037c26d497d81bba11fef272
Author: Dimitris Soumis <[email protected]>
AuthorDate: Tue Apr 21 23:49:54 2026 +0300
Fix race condition in StandardContext.getServletContext()
The context field was not volatile and getServletContext()was not
synchronized, allowing concurrent threads to create competing
ApplicationContext instances during reload. This could cause the TEMPDIR
servlet context attribute to be permanently lost.
---
java/org/apache/catalina/core/StandardContext.java | 16 ++++---
.../apache/catalina/core/TestStandardContext.java | 54 ++++++++++++++++++++++
webapps/docs/changelog.xml | 8 ++++
3 files changed, 72 insertions(+), 6 deletions(-)
diff --git a/java/org/apache/catalina/core/StandardContext.java
b/java/org/apache/catalina/core/StandardContext.java
index 9e880fb32e..22e7bea321 100644
--- a/java/org/apache/catalina/core/StandardContext.java
+++ b/java/org/apache/catalina/core/StandardContext.java
@@ -280,7 +280,7 @@ public class StandardContext extends ContainerBase
implements Context, Notificat
/**
* The ServletContext implementation associated with this Context.
*/
- protected ApplicationContext context = null;
+ protected volatile ApplicationContext context = null;
/**
* The wrapped version of the associated ServletContext that is presented
to listeners that are required to have
@@ -2185,12 +2185,16 @@ public class StandardContext extends ContainerBase
implements Context, Notificat
@Override
public ServletContext getServletContext() {
- // This method is called multiple times during context start which is
single threaded
- // so there is no concurrency issue
+ // Outer check avoids locking when context already exists and
+ // inner check prevents duplicate creation when multiple threads race
past the outer check.
if (context == null) {
- context = new ApplicationContext(this);
- if (altDDName != null) {
- context.setAttribute(Globals.ALT_DD_ATTR, altDDName);
+ synchronized (this) {
+ if (context == null) {
+ context = new ApplicationContext(this);
+ if (altDDName != null) {
+ context.setAttribute(Globals.ALT_DD_ATTR, altDDName);
+ }
+ }
}
}
return context.getFacade();
diff --git a/test/org/apache/catalina/core/TestStandardContext.java
b/test/org/apache/catalina/core/TestStandardContext.java
index 02655e3fa8..d1a90d30cd 100644
--- a/test/org/apache/catalina/core/TestStandardContext.java
+++ b/test/org/apache/catalina/core/TestStandardContext.java
@@ -19,9 +19,13 @@ package org.apache.catalina.core;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.atomic.AtomicReference;
import jakarta.servlet.FilterChain;
import jakarta.servlet.GenericFilter;
@@ -1038,6 +1042,56 @@ public class TestStandardContext extends TomcatBaseTest {
Assert.assertTrue(lifecycleListenerOk);
}
+ @Test
+ public void testGetServletContextReturnsSameInstanceUnderConcurrency()
throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+ File appDir = new File(tomcat.getHost().getAppBaseFile(), "ROOT");
+ if (!appDir.mkdirs() && !appDir.isDirectory()) {
+ Assert.fail("Unable to create appDir");
+ }
+
+ StandardContext standardContext = (StandardContext)
tomcat.addContext("", appDir.getAbsolutePath());
+ tomcat.start();
+
+ // Null the context field to simulate the window during reload
+ standardContext.context = null;
+
+ int numThreads = 20;
+ CyclicBarrier barrier = new CyclicBarrier(numThreads);
+ List<Thread> threads = new ArrayList<>();
+ ServletContext[] results = new ServletContext[numThreads];
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+
+ for (int numOfThread = 0; numOfThread < numThreads; numOfThread++) {
+ final int index = numOfThread;
+ Thread thread = new Thread(() -> {
+ try {
+ barrier.await();
+ results[index] = standardContext.getServletContext();
+ } catch (Throwable ex) {
+ failure.set(ex);
+ }
+ });
+ thread.start();
+ threads.add(thread);
+ }
+
+ for (Thread thread : threads) {
+ thread.join(5000);
+ }
+
+ if (failure.get() != null) {
+ Assert.fail("Thread failed: " + failure.get());
+ }
+
+ ServletContext first = results[0];
+ Assert.assertNotNull(first);
+ for (int i = 1; i < numThreads; i++) {
+ Assert.assertSame(first, results[i]);
+ }
+ }
+
+
private static boolean customWrapperClassOk = false;
public static class MyWrapperClass extends StandardWrapper {
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 72ebd79dd9..f4a4e99c62 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -137,6 +137,14 @@
Requests without authentication were receiving 403 responses rather
than
401 responses. (markt)
</fix>
+ <fix>
+ Fix a race condition in
<code>StandardContext.getServletContext()</code>
+ that could cause the <code>jakarta.servlet.context.tempdir</code>
+ attribute to be lost during a context reload. Make the
+ <code>context</code> field volatile and use locking to
+ ensure only one <code>ApplicationContext</code> instance is created.
+ (dsoumis)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]