This is an automated email from the ASF dual-hosted git repository.
dsoumis pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new a5ffaf6911 Fix race condition in StandardContext.getServletContext()
a5ffaf6911 is described below
commit a5ffaf6911fbb529b95374885aca5ebc4b9a7e80
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 c773e1c011..a8ffcba938 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
@@ -2118,12 +2118,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 f3895533e3..9e0b5a20dd 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 javax.servlet.FilterChain;
import javax.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 b249b8c4cf..cc6aa6ea6a 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]