Apache Tomcat: RCE due to TOCTOU issue in JSP compilation
Description
Time-of-check Time-of-use (TOCTOU) Race Condition vulnerability during JSP compilation in Apache Tomcat permits an RCE on case insensitive file systems when the default servlet is enabled for write (non-default configuration).
This issue affects Apache Tomcat: from 11.0.0-M1 through 11.0.1, from 10.1.0-M1 through 10.1.33, from 9.0.0.M1 through 9.0.97.
The following versions were EOL at the time the CVE was created but are known to be affected: 8.5.0 though 8.5.100. Other, older, EOL versions may also be affected.
Users are recommended to upgrade to version 11.0.2, 10.1.34 or 9.0.98, which fixes the issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A TOCTOU race condition during JSP compilation in Apache Tomcat allows remote code execution on case-insensitive file systems with the default servlet enabled for write.
Vulnerability
Overview
CVE-2024-50379 is a Time-of-Check Time-of-Use (TOCTOU) race condition vulnerability within the JSP compilation mechanism of Apache Tomcat. The flaw occurs on case-insensitive file systems when the default servlet is configured with write access enabled, which is a non-default configuration. This race condition permits an attacker to achieve remote code execution (RCE) by manipulating files during the compilation process [1][2][3].
Attack
Vector and Prerequisites
Exploitation requires the default servlet to have write permissions enabled, a setting not enabled by default. On case-insensitive file systems, an attacker can trigger a race window by rapidly submitting a crafted request that causes the server to compile a JSP file while simultaneously modifying the underlying file. This race can lead to the execution of attacker-controlled code. No authentication is explicitly mentioned as a prerequisite, but the attacker must have write access to a web-accessible directory [4].
Impact and
Affected Versions
Successful exploitation results in arbitrary code execution within the Tomcat server context. The vulnerability impacts Apache Tomcat versions: 11.0.0-M1 through 11.0.1, 10.1.0-M1 through 10.1.33, and 9.0.0.M1 through 9.0.97. Additionally, versions 8.5.0 through 8.5.100 (which were end-of-life at the time of disclosure) are also known to be affected [4].
Mitigation
Apache has released fixed versions: 11.0.2, 10.1.34, and 9.0.98. Users are strongly advised to upgrade to these patched releases. As a mitigation, administrators can ensure the default servlet write functionality is disabled if not required. There is no indication that this vulnerability is being exploited in the wild at the time of publication [1][2][3].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.tomcat:tomcat-catalinaMaven | >= 11.0.0-M1, < 11.0.2 | 11.0.2 |
org.apache.tomcat:tomcat-catalinaMaven | >= 10.1.0-M1, < 10.1.34 | 10.1.34 |
org.apache.tomcat:tomcat-catalinaMaven | >= 9.0.0.M1, < 9.0.98 | 9.0.98 |
org.apache.tomcat.embed:tomcat-embed-coreMaven | >= 11.0.0-M1, < 11.0.2 | 11.0.2 |
org.apache.tomcat.embed:tomcat-embed-coreMaven | >= 10.1.0-M1, < 10.1.34 | 10.1.34 |
org.apache.tomcat.embed:tomcat-embed-coreMaven | >= 9.0.0.M1, < 9.0.98 | 9.0.98 |
org.apache.tomcat:tomcat-catalinaMaven | >= 8.5.0, <= 8.5.100 | — |
org.apache.tomcat.embed:tomcat-embed-coreMaven | >= 8.5.0, <= 8.5.100 | — |
Affected products
58- osv-coords56 versionspkg:apk/chainguard/camunda-zeebepkg:apk/chainguard/camunda-zeebe-compatpkg:apk/chainguard/thingsboardpkg:apk/chainguard/thingsboard-tb-js-executorpkg:apk/chainguard/thingsboard-tb-mqtt-transportpkg:apk/chainguard/thingsboard-tb-nodepkg:apk/chainguard/thingsboard-tb-web-uipkg:apk/wolfi/thingsboardpkg:apk/wolfi/thingsboard-tb-js-executorpkg:apk/wolfi/thingsboard-tb-mqtt-transportpkg:apk/wolfi/thingsboard-tb-nodepkg:apk/wolfi/thingsboard-tb-web-uipkg:bitnami/tomcatpkg:maven/org.apache.tomcat.embed/tomcat-embed-corepkg:maven/org.apache.tomcat/tomcat-catalinapkg:rpm/almalinux/tomcatpkg:rpm/almalinux/tomcat9pkg:rpm/almalinux/tomcat9-admin-webappspkg:rpm/almalinux/tomcat9-docs-webapppkg:rpm/almalinux/tomcat9-el-3.0-apipkg:rpm/almalinux/tomcat9-jsp-2.3-apipkg:rpm/almalinux/tomcat9-libpkg:rpm/almalinux/tomcat9-servlet-4.0-apipkg:rpm/almalinux/tomcat9-webappspkg:rpm/almalinux/tomcat-admin-webappspkg:rpm/almalinux/tomcat-docs-webapppkg:rpm/almalinux/tomcat-el-3.0-apipkg:rpm/almalinux/tomcat-jsp-2.3-apipkg:rpm/almalinux/tomcat-libpkg:rpm/almalinux/tomcat-servlet-4.0-apipkg:rpm/almalinux/tomcat-webappspkg:rpm/opensuse/tomcat10&distro=openSUSE%20Leap%2015.6pkg:rpm/opensuse/tomcat10&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/tomcat&distro=openSUSE%20Leap%2015.6pkg:rpm/opensuse/tomcat&distro=openSUSE%20Tumbleweedpkg:rpm/suse/tomcat10&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP5-ESPOSpkg:rpm/suse/tomcat10&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP5-LTSSpkg:rpm/suse/tomcat10&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Web%20and%20Scripting%2015%20SP6pkg:rpm/suse/tomcat10&distro=SUSE%20Linux%20Enterprise%20Server%2015%20SP5-LTSSpkg:rpm/suse/tomcat10&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015%20SP5pkg:rpm/suse/tomcat&distro=SUSE%20Enterprise%20Storage%207.1pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP3-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP4-ESPOSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP4-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP5-ESPOSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP5-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Web%20and%20Scripting%2015%20SP6pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%2012%20SP5-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%2015%20SP3-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%2015%20SP4-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%2015%20SP5-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015%20SP3pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015%20SP4pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015%20SP5pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%20LTSS%20Extended%20Security%2012%20SP5pkg:rpm/suse/tomcat&distro=SUSE%20Manager%20Server%204.3
< 8.6.6-r2+ 55 more
- (no CPE)range: < 8.6.6-r2
- (no CPE)range: < 8.6.6-r2
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: < 3.9-r1
- (no CPE)range: >= 9.0.0, < 9.0.98
- (no CPE)range: >= 11.0.0-M1, < 11.0.2
- (no CPE)range: >= 11.0.0-M1, < 11.0.2
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-5.el10_0.1
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 1:9.0.87-1.el8_10.4
- (no CPE)range: < 10.1.34-150200.5.31.1
- (no CPE)range: < 10.1.34-1.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-1.1
- (no CPE)range: < 10.1.34-150200.5.31.1
- (no CPE)range: < 10.1.34-150200.5.31.1
- (no CPE)range: < 10.1.34-150200.5.31.1
- (no CPE)range: < 10.1.34-150200.5.31.1
- (no CPE)range: < 10.1.34-150200.5.31.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.36-3.136.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.98-150200.74.1
- (no CPE)range: < 9.0.36-3.136.1
- (no CPE)range: < 9.0.98-150200.74.1
- Apache Software Foundation/Apache Tomcatv5Range: 11.0.0-M1
Patches
6631500b0c9b2Fix for inconsistent metadata was incomplete
2 files changed · +6 −0
java/org/apache/catalina/webresources/DirResourceSet.java+2 −0 modified@@ -382,6 +382,7 @@ public ResourceLock lockForRead(String path) { resourceLock = resourceLocksByPath.get(key); if (resourceLock == null) { resourceLock = new ResourceLock(key); + resourceLocksByPath.put(key, resourceLock); } resourceLock.count.incrementAndGet(); } @@ -419,6 +420,7 @@ public ResourceLock lockForWrite(String path) { resourceLock = resourceLocksByPath.get(key); if (resourceLock == null) { resourceLock = new ResourceLock(key); + resourceLocksByPath.put(key, resourceLock); } resourceLock.count.incrementAndGet(); }
webapps/docs/changelog.xml+4 −0 modified@@ -128,6 +128,10 @@ constructors or property related methods throw a checked exception. (remm) </fix> + <fix> + The previous fix for incosistent resource metadata during concurrent + reads and writes was incomplete. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote">
05ddeeaa54dfFix for inconsistent metadata was incomplete
2 files changed · +6 −0
java/org/apache/catalina/webresources/DirResourceSet.java+2 −0 modified@@ -382,6 +382,7 @@ public ResourceLock lockForRead(String path) { resourceLock = resourceLocksByPath.get(key); if (resourceLock == null) { resourceLock = new ResourceLock(key); + resourceLocksByPath.put(key, resourceLock); } resourceLock.count.incrementAndGet(); } @@ -419,6 +420,7 @@ public ResourceLock lockForWrite(String path) { resourceLock = resourceLocksByPath.get(key); if (resourceLock == null) { resourceLock = new ResourceLock(key); + resourceLocksByPath.put(key, resourceLock); } resourceLock.count.incrementAndGet(); }
webapps/docs/changelog.xml+4 −0 modified@@ -128,6 +128,10 @@ constructors or property related methods throw a checked exception. (remm) </fix> + <fix> + The previous fix for incosistent resource metadata during concurrent + reads and writes was incomplete. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote">
684247ae85faFix for inconsistent metadata was incomplete
2 files changed · +6 −0
java/org/apache/catalina/webresources/DirResourceSet.java+2 −0 modified@@ -382,6 +382,7 @@ public ResourceLock lockForRead(String path) { resourceLock = resourceLocksByPath.get(key); if (resourceLock == null) { resourceLock = new ResourceLock(key); + resourceLocksByPath.put(key, resourceLock); } resourceLock.count.incrementAndGet(); } @@ -419,6 +420,7 @@ public ResourceLock lockForWrite(String path) { resourceLock = resourceLocksByPath.get(key); if (resourceLock == null) { resourceLock = new ResourceLock(key); + resourceLocksByPath.put(key, resourceLock); } resourceLock.count.incrementAndGet(); }
webapps/docs/changelog.xml+4 −0 modified@@ -128,6 +128,10 @@ constructors or property related methods throw a checked exception. (remm) </fix> + <fix> + The previous fix for incosistent resource metadata during concurrent + reads and writes was incomplete. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote">
8554f6b1722bFix inconsistent resource metadata with current GET and PUT/DELETE
4 files changed · +273 −29
java/org/apache/catalina/WebResourceLockSet.java+73 −0 added@@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Interface implemented by {@link WebResourceSet} implementations that wish to provide locking functionality. + */ +public interface WebResourceLockSet { + + /** + * Lock the resource at the provided path for reading. The resource is not required to exist. Read locks are not + * exclusive. + * + * @param path The path to the resource to be locked for reading + * + * @return The {@link ResourceLock} that must be passed to {@link #unlockForRead(ResourceLock)} to release the lock + */ + ResourceLock lockForRead(String path); + + /** + * Release a read lock from the resource associated with the given {@link ResourceLock}. + * + * @param resourceLock The {@link ResourceLock} associated with the resource for which a read lock should be + * released + */ + void unlockForRead(ResourceLock resourceLock); + + /** + * Lock the resource at the provided path for writing. The resource is not required to exist. Write locks are + * exclusive. + * + * @param path The path to the resource to be locked for writing + * + * @return The {@link ResourceLock} that must be passed to {@link #unlockForWrite(ResourceLock)} to release the lock + */ + ResourceLock lockForWrite(String path); + + /** + * Release the write lock from the resource associated with the given {@link ResourceLock}. + * + * @param resourceLock The {@link ResourceLock} associated with the resource for which the write lock should be + * released + */ + void unlockForWrite(ResourceLock resourceLock); + + + class ResourceLock { + public final AtomicInteger count = new AtomicInteger(0); + public final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(); + public final String key; + + public ResourceLock(String key) { + this.key = key; + } + } +}
java/org/apache/catalina/webresources/DirResourceSet.java+171 −28 modified@@ -22,24 +22,35 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.jar.Manifest; import org.apache.catalina.LifecycleException; import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceLockSet; import org.apache.catalina.WebResourceRoot; import org.apache.catalina.WebResourceRoot.ResourceSetType; import org.apache.catalina.util.ResourceSet; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.RequestUtil; /** * Represents a {@link org.apache.catalina.WebResourceSet} based on a directory. */ -public class DirResourceSet extends AbstractFileResourceSet { +public class DirResourceSet extends AbstractFileResourceSet implements WebResourceLockSet { private static final Log log = LogFactory.getLog(DirResourceSet.class); + private boolean caseSensitive = true; + + private Map<String,ResourceLock> resourceLocksByPath = new HashMap<>(); + private Object resourceLocksByPathLock = new Object(); + + /** * A no argument constructor is required for this to work with the digester. */ @@ -91,22 +102,33 @@ public WebResource getResource(String path) { String webAppMount = getWebAppMount(); WebResourceRoot root = getRoot(); if (path.startsWith(webAppMount)) { - File f = file(path.substring(webAppMount.length()), false); - if (f == null) { - return new EmptyResource(root, path); - } - if (!f.exists()) { - return new EmptyResource(root, path, f); - } - if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { - path = path + '/'; + /* + * Lock the path for reading until the WebResource has been constructed. The lock prevents concurrent reads + * and writes (e.g. HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource + * where some of the fields are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = lockForRead(path); + try { + File f = file(path.substring(webAppMount.length()), false); + if (f == null) { + return new EmptyResource(root, path); + } + if (!f.exists()) { + return new EmptyResource(root, path, f); + } + if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { + path = path + '/'; + } + return new FileResource(root, path, f, isReadOnly(), getManifest(), this, lock.key); + } finally { + unlockForRead(lock); } - return new FileResource(root, path, f, isReadOnly(), getManifest()); } else { return new EmptyResource(root, path); } } + @Override public String[] list(String path) { checkPath(path); @@ -246,32 +268,42 @@ public boolean write(String path, InputStream is, boolean overwrite) { return false; } - File dest = null; String webAppMount = getWebAppMount(); - if (path.startsWith(webAppMount)) { + if (!path.startsWith(webAppMount)) { + return false; + } + + File dest = null; + /* + * Lock the path for writing until the write is complete. The lock prevents concurrent reads and writes (e.g. + * HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields + * are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = lockForWrite(path); + try { dest = file(path.substring(webAppMount.length()), false); if (dest == null) { return false; } - } else { - return false; - } - if (dest.exists() && !overwrite) { - return false; - } + if (dest.exists() && !overwrite) { + return false; + } - try { - if (overwrite) { - Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); - } else { - Files.copy(is, dest.toPath()); + try { + if (overwrite) { + Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } else { + Files.copy(is, dest.toPath()); + } + } catch (IOException ioe) { + return false; } - } catch (IOException ioe) { - return false; - } - return true; + return true; + } finally { + unlockForWrite(lock); + } } @Override @@ -286,6 +318,7 @@ protected void checkType(File file) { @Override protected void initInternal() throws LifecycleException { super.initInternal(); + caseSensitive = isCaseSensitive(); // Is this an exploded web application? if (getWebAppMount().equals("")) { // Look for a manifest @@ -299,4 +332,114 @@ protected void initInternal() throws LifecycleException { } } } + + + /* + * Determines if this ResourceSet is based on a case sensitive file system or not. + */ + private boolean isCaseSensitive() { + try { + String canonicalPath = getFileBase().getCanonicalPath(); + File upper = new File(canonicalPath.toUpperCase(Locale.ENGLISH)); + if (!canonicalPath.equals(upper.getCanonicalPath())) { + return true; + } + File lower = new File(canonicalPath.toLowerCase(Locale.ENGLISH)); + if (!canonicalPath.equals(lower.getCanonicalPath())) { + return true; + } + /* + * Both upper and lower case versions of the current fileBase have the same canonical path so the file + * system must be case insensitive. + */ + } catch (IOException ioe) { + log.warn(sm.getString("dirResourceSet.isCaseSensitive.fail", getFileBase().getAbsolutePath()), ioe); + } + + return false; + } + + + private String getLockKey(String path) { + // Normalize path to ensure that the same key is used for the same path. + String normalisedPath = RequestUtil.normalize(path); + if (caseSensitive) { + return normalisedPath; + } + return normalisedPath.toLowerCase(Locale.ENGLISH); + } + + + @Override + public ResourceLock lockForRead(String path) { + String key = getLockKey(path); + ResourceLock resourceLock = null; + synchronized (resourceLocksByPathLock) { + /* + * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has + * a consistent view of the currently "in-use" ResourceLocks. + */ + resourceLock = resourceLocksByPath.get(key); + if (resourceLock == null) { + resourceLock = new ResourceLock(key); + } + resourceLock.count.incrementAndGet(); + } + // Obtain the lock outside the sync as it will block if there is a current write lock. + resourceLock.reentrantLock.readLock().lock(); + return resourceLock; + } + + + @Override + public void unlockForRead(ResourceLock resourceLock) { + // Unlock outside the sync as there is no need to do it inside. + resourceLock.reentrantLock.readLock().unlock(); + synchronized (resourceLocksByPathLock) { + /* + * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that + * map always has a consistent view of the currently "in-use" ResourceLocks. + */ + if (resourceLock.count.decrementAndGet() == 0) { + resourceLocksByPath.remove(resourceLock.key); + } + } + } + + + @Override + public ResourceLock lockForWrite(String path) { + String key = getLockKey(path); + ResourceLock resourceLock = null; + synchronized (resourceLocksByPathLock) { + /* + * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has + * a consistent view of the currently "in-use" ResourceLocks. + */ + resourceLock = resourceLocksByPath.get(key); + if (resourceLock == null) { + resourceLock = new ResourceLock(key); + } + resourceLock.count.incrementAndGet(); + } + // Obtain the lock outside the sync as it will block if there are any other current locks. + resourceLock.reentrantLock.writeLock().lock(); + return resourceLock; + } + + + @Override + public void unlockForWrite(ResourceLock resourceLock) { + // Unlock outside the sync as there is no need to do it inside. + resourceLock.reentrantLock.writeLock().unlock(); + synchronized (resourceLocksByPathLock) { + /* + * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that + * map always has a consistent view of the currently "in-use" ResourceLocks. + */ + if (resourceLock.count.decrementAndGet() == 0) { + resourceLocksByPath.remove(resourceLock.key); + } + } + } }
java/org/apache/catalina/webresources/FileResource.java+28 −1 modified@@ -31,6 +31,8 @@ import java.security.cert.Certificate; import java.util.jar.Manifest; +import org.apache.catalina.WebResourceLockSet; +import org.apache.catalina.WebResourceLockSet.ResourceLock; import org.apache.catalina.WebResourceRoot; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -62,10 +64,20 @@ public class FileResource extends AbstractResource { private final boolean readOnly; private final Manifest manifest; private final boolean needConvert; + private final WebResourceLockSet lockSet; + private final String lockKey; public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest) { + this(root, webAppPath, resource, readOnly, manifest, null, null); + } + + + public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest, + WebResourceLockSet lockSet, String lockKey) { super(root, webAppPath); this.resource = resource; + this.lockSet = lockSet; + this.lockKey = lockKey; if (webAppPath.charAt(webAppPath.length() - 1) == '/') { String realName = resource.getName() + '/'; @@ -117,7 +129,22 @@ public boolean delete() { if (readOnly) { return false; } - return resource.delete(); + /* + * Lock the path for writing until the delete is complete. The lock prevents concurrent reads and writes (e.g. + * HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields + * are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = null; + if (lockSet != null) { + lock = lockSet.lockForWrite(lockKey); + } + try { + return resource.delete(); + } finally { + if (lockSet != null) { + lockSet.unlockForWrite(lock); + } + } } @Override
java/org/apache/catalina/webresources/LocalStrings.properties+1 −0 modified@@ -36,6 +36,7 @@ cachedResource.invalidURL=Unable to create an instance of CachedResourceURLStrea classpathUrlStreamHandler.notFound=Unable to load the resource [{0}] using the thread context class loader or the current class''s class loader +dirResourceSet.isCaseSensitive.fail=Error trying to determine if file system at [{0}] is case sensitive so assuming it is not case sensitive dirResourceSet.manifestFail=Failed to read manifest from [{0}] dirResourceSet.notDirectory=The directory specified by base and internal path [{0}]{1}[{2}] does not exist. dirResourceSet.writeNpe=The input stream may not be null
cc7a98b57c6dFix inconsistent resource metadata with current GET and PUT/DELETE
4 files changed · +273 −29
java/org/apache/catalina/WebResourceLockSet.java+73 −0 added@@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Interface implemented by {@link WebResourceSet} implementations that wish to provide locking functionality. + */ +public interface WebResourceLockSet { + + /** + * Lock the resource at the provided path for reading. The resource is not required to exist. Read locks are not + * exclusive. + * + * @param path The path to the resource to be locked for reading + * + * @return The {@link ResourceLock} that must be passed to {@link #unlockForRead(ResourceLock)} to release the lock + */ + ResourceLock lockForRead(String path); + + /** + * Release a read lock from the resource associated with the given {@link ResourceLock}. + * + * @param resourceLock The {@link ResourceLock} associated with the resource for which a read lock should be + * released + */ + void unlockForRead(ResourceLock resourceLock); + + /** + * Lock the resource at the provided path for writing. The resource is not required to exist. Write locks are + * exclusive. + * + * @param path The path to the resource to be locked for writing + * + * @return The {@link ResourceLock} that must be passed to {@link #unlockForWrite(ResourceLock)} to release the lock + */ + ResourceLock lockForWrite(String path); + + /** + * Release the write lock from the resource associated with the given {@link ResourceLock}. + * + * @param resourceLock The {@link ResourceLock} associated with the resource for which the write lock should be + * released + */ + void unlockForWrite(ResourceLock resourceLock); + + + class ResourceLock { + public final AtomicInteger count = new AtomicInteger(0); + public final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(); + public final String key; + + public ResourceLock(String key) { + this.key = key; + } + } +}
java/org/apache/catalina/webresources/DirResourceSet.java+171 −28 modified@@ -22,24 +22,35 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.jar.Manifest; import org.apache.catalina.LifecycleException; import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceLockSet; import org.apache.catalina.WebResourceRoot; import org.apache.catalina.WebResourceRoot.ResourceSetType; import org.apache.catalina.util.ResourceSet; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.RequestUtil; /** * Represents a {@link org.apache.catalina.WebResourceSet} based on a directory. */ -public class DirResourceSet extends AbstractFileResourceSet { +public class DirResourceSet extends AbstractFileResourceSet implements WebResourceLockSet { private static final Log log = LogFactory.getLog(DirResourceSet.class); + private boolean caseSensitive = true; + + private Map<String,ResourceLock> resourceLocksByPath = new HashMap<>(); + private Object resourceLocksByPathLock = new Object(); + + /** * A no argument constructor is required for this to work with the digester. */ @@ -91,22 +102,33 @@ public WebResource getResource(String path) { String webAppMount = getWebAppMount(); WebResourceRoot root = getRoot(); if (path.startsWith(webAppMount)) { - File f = file(path.substring(webAppMount.length()), false); - if (f == null) { - return new EmptyResource(root, path); - } - if (!f.exists()) { - return new EmptyResource(root, path, f); - } - if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { - path = path + '/'; + /* + * Lock the path for reading until the WebResource has been constructed. The lock prevents concurrent reads + * and writes (e.g. HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource + * where some of the fields are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = lockForRead(path); + try { + File f = file(path.substring(webAppMount.length()), false); + if (f == null) { + return new EmptyResource(root, path); + } + if (!f.exists()) { + return new EmptyResource(root, path, f); + } + if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { + path = path + '/'; + } + return new FileResource(root, path, f, isReadOnly(), getManifest(), this, lock.key); + } finally { + unlockForRead(lock); } - return new FileResource(root, path, f, isReadOnly(), getManifest()); } else { return new EmptyResource(root, path); } } + @Override public String[] list(String path) { checkPath(path); @@ -246,32 +268,42 @@ public boolean write(String path, InputStream is, boolean overwrite) { return false; } - File dest = null; String webAppMount = getWebAppMount(); - if (path.startsWith(webAppMount)) { + if (!path.startsWith(webAppMount)) { + return false; + } + + File dest = null; + /* + * Lock the path for writing until the write is complete. The lock prevents concurrent reads and writes (e.g. + * HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields + * are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = lockForWrite(path); + try { dest = file(path.substring(webAppMount.length()), false); if (dest == null) { return false; } - } else { - return false; - } - if (dest.exists() && !overwrite) { - return false; - } + if (dest.exists() && !overwrite) { + return false; + } - try { - if (overwrite) { - Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); - } else { - Files.copy(is, dest.toPath()); + try { + if (overwrite) { + Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } else { + Files.copy(is, dest.toPath()); + } + } catch (IOException ioe) { + return false; } - } catch (IOException ioe) { - return false; - } - return true; + return true; + } finally { + unlockForWrite(lock); + } } @Override @@ -286,6 +318,7 @@ protected void checkType(File file) { @Override protected void initInternal() throws LifecycleException { super.initInternal(); + caseSensitive = isCaseSensitive(); // Is this an exploded web application? if (getWebAppMount().equals("")) { // Look for a manifest @@ -299,4 +332,114 @@ protected void initInternal() throws LifecycleException { } } } + + + /* + * Determines if this ResourceSet is based on a case sensitive file system or not. + */ + private boolean isCaseSensitive() { + try { + String canonicalPath = getFileBase().getCanonicalPath(); + File upper = new File(canonicalPath.toUpperCase(Locale.ENGLISH)); + if (!canonicalPath.equals(upper.getCanonicalPath())) { + return true; + } + File lower = new File(canonicalPath.toLowerCase(Locale.ENGLISH)); + if (!canonicalPath.equals(lower.getCanonicalPath())) { + return true; + } + /* + * Both upper and lower case versions of the current fileBase have the same canonical path so the file + * system must be case insensitive. + */ + } catch (IOException ioe) { + log.warn(sm.getString("dirResourceSet.isCaseSensitive.fail", getFileBase().getAbsolutePath()), ioe); + } + + return false; + } + + + private String getLockKey(String path) { + // Normalize path to ensure that the same key is used for the same path. + String normalisedPath = RequestUtil.normalize(path); + if (caseSensitive) { + return normalisedPath; + } + return normalisedPath.toLowerCase(Locale.ENGLISH); + } + + + @Override + public ResourceLock lockForRead(String path) { + String key = getLockKey(path); + ResourceLock resourceLock = null; + synchronized (resourceLocksByPathLock) { + /* + * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has + * a consistent view of the currently "in-use" ResourceLocks. + */ + resourceLock = resourceLocksByPath.get(key); + if (resourceLock == null) { + resourceLock = new ResourceLock(key); + } + resourceLock.count.incrementAndGet(); + } + // Obtain the lock outside the sync as it will block if there is a current write lock. + resourceLock.reentrantLock.readLock().lock(); + return resourceLock; + } + + + @Override + public void unlockForRead(ResourceLock resourceLock) { + // Unlock outside the sync as there is no need to do it inside. + resourceLock.reentrantLock.readLock().unlock(); + synchronized (resourceLocksByPathLock) { + /* + * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that + * map always has a consistent view of the currently "in-use" ResourceLocks. + */ + if (resourceLock.count.decrementAndGet() == 0) { + resourceLocksByPath.remove(resourceLock.key); + } + } + } + + + @Override + public ResourceLock lockForWrite(String path) { + String key = getLockKey(path); + ResourceLock resourceLock = null; + synchronized (resourceLocksByPathLock) { + /* + * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has + * a consistent view of the currently "in-use" ResourceLocks. + */ + resourceLock = resourceLocksByPath.get(key); + if (resourceLock == null) { + resourceLock = new ResourceLock(key); + } + resourceLock.count.incrementAndGet(); + } + // Obtain the lock outside the sync as it will block if there are any other current locks. + resourceLock.reentrantLock.writeLock().lock(); + return resourceLock; + } + + + @Override + public void unlockForWrite(ResourceLock resourceLock) { + // Unlock outside the sync as there is no need to do it inside. + resourceLock.reentrantLock.writeLock().unlock(); + synchronized (resourceLocksByPathLock) { + /* + * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that + * map always has a consistent view of the currently "in-use" ResourceLocks. + */ + if (resourceLock.count.decrementAndGet() == 0) { + resourceLocksByPath.remove(resourceLock.key); + } + } + } }
java/org/apache/catalina/webresources/FileResource.java+28 −1 modified@@ -31,6 +31,8 @@ import java.security.cert.Certificate; import java.util.jar.Manifest; +import org.apache.catalina.WebResourceLockSet; +import org.apache.catalina.WebResourceLockSet.ResourceLock; import org.apache.catalina.WebResourceRoot; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -62,10 +64,20 @@ public class FileResource extends AbstractResource { private final boolean readOnly; private final Manifest manifest; private final boolean needConvert; + private final WebResourceLockSet lockSet; + private final String lockKey; public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest) { + this(root, webAppPath, resource, readOnly, manifest, null, null); + } + + + public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest, + WebResourceLockSet lockSet, String lockKey) { super(root, webAppPath); this.resource = resource; + this.lockSet = lockSet; + this.lockKey = lockKey; if (webAppPath.charAt(webAppPath.length() - 1) == '/') { String realName = resource.getName() + '/'; @@ -117,7 +129,22 @@ public boolean delete() { if (readOnly) { return false; } - return resource.delete(); + /* + * Lock the path for writing until the delete is complete. The lock prevents concurrent reads and writes (e.g. + * HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields + * are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = null; + if (lockSet != null) { + lock = lockSet.lockForWrite(lockKey); + } + try { + return resource.delete(); + } finally { + if (lockSet != null) { + lockSet.unlockForWrite(lock); + } + } } @Override
java/org/apache/catalina/webresources/LocalStrings.properties+1 −0 modified@@ -33,6 +33,7 @@ cachedResource.invalidURL=Unable to create an instance of CachedResourceURLStrea classpathUrlStreamHandler.notFound=Unable to load the resource [{0}] using the thread context class loader or the current class''s class loader +dirResourceSet.isCaseSensitive.fail=Error trying to determine if file system at [{0}] is case sensitive so assuming it is not case sensitive dirResourceSet.manifestFail=Failed to read manifest from [{0}] dirResourceSet.notDirectory=The directory specified by base and internal path [{0}]{1}[{2}] does not exist. dirResourceSet.writeNpe=The input stream may not be null
43b507ebac9dFix inconsistent resource metadata with current GET and PUT/DELETE
4 files changed · +273 −29
java/org/apache/catalina/WebResourceLockSet.java+73 −0 added@@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Interface implemented by {@link WebResourceSet} implementations that wish to provide locking functionality. + */ +public interface WebResourceLockSet { + + /** + * Lock the resource at the provided path for reading. The resource is not required to exist. Read locks are not + * exclusive. + * + * @param path The path to the resource to be locked for reading + * + * @return The {@link ResourceLock} that must be passed to {@link #unlockForRead(ResourceLock)} to release the lock + */ + ResourceLock lockForRead(String path); + + /** + * Release a read lock from the resource associated with the given {@link ResourceLock}. + * + * @param resourceLock The {@link ResourceLock} associated with the resource for which a read lock should be + * released + */ + void unlockForRead(ResourceLock resourceLock); + + /** + * Lock the resource at the provided path for writing. The resource is not required to exist. Write locks are + * exclusive. + * + * @param path The path to the resource to be locked for writing + * + * @return The {@link ResourceLock} that must be passed to {@link #unlockForWrite(ResourceLock)} to release the lock + */ + ResourceLock lockForWrite(String path); + + /** + * Release the write lock from the resource associated with the given {@link ResourceLock}. + * + * @param resourceLock The {@link ResourceLock} associated with the resource for which the write lock should be + * released + */ + void unlockForWrite(ResourceLock resourceLock); + + + class ResourceLock { + public final AtomicInteger count = new AtomicInteger(0); + public final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(); + public final String key; + + public ResourceLock(String key) { + this.key = key; + } + } +}
java/org/apache/catalina/webresources/DirResourceSet.java+171 −28 modified@@ -22,24 +22,35 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.jar.Manifest; import org.apache.catalina.LifecycleException; import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceLockSet; import org.apache.catalina.WebResourceRoot; import org.apache.catalina.WebResourceRoot.ResourceSetType; import org.apache.catalina.util.ResourceSet; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.RequestUtil; /** * Represents a {@link org.apache.catalina.WebResourceSet} based on a directory. */ -public class DirResourceSet extends AbstractFileResourceSet { +public class DirResourceSet extends AbstractFileResourceSet implements WebResourceLockSet { private static final Log log = LogFactory.getLog(DirResourceSet.class); + private boolean caseSensitive = true; + + private Map<String,ResourceLock> resourceLocksByPath = new HashMap<>(); + private Object resourceLocksByPathLock = new Object(); + + /** * A no argument constructor is required for this to work with the digester. */ @@ -91,22 +102,33 @@ public WebResource getResource(String path) { String webAppMount = getWebAppMount(); WebResourceRoot root = getRoot(); if (path.startsWith(webAppMount)) { - File f = file(path.substring(webAppMount.length()), false); - if (f == null) { - return new EmptyResource(root, path); - } - if (!f.exists()) { - return new EmptyResource(root, path, f); - } - if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { - path = path + '/'; + /* + * Lock the path for reading until the WebResource has been constructed. The lock prevents concurrent reads + * and writes (e.g. HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource + * where some of the fields are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = lockForRead(path); + try { + File f = file(path.substring(webAppMount.length()), false); + if (f == null) { + return new EmptyResource(root, path); + } + if (!f.exists()) { + return new EmptyResource(root, path, f); + } + if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { + path = path + '/'; + } + return new FileResource(root, path, f, isReadOnly(), getManifest(), this, lock.key); + } finally { + unlockForRead(lock); } - return new FileResource(root, path, f, isReadOnly(), getManifest()); } else { return new EmptyResource(root, path); } } + @Override public String[] list(String path) { checkPath(path); @@ -246,32 +268,42 @@ public boolean write(String path, InputStream is, boolean overwrite) { return false; } - File dest = null; String webAppMount = getWebAppMount(); - if (path.startsWith(webAppMount)) { + if (!path.startsWith(webAppMount)) { + return false; + } + + File dest = null; + /* + * Lock the path for writing until the write is complete. The lock prevents concurrent reads and writes (e.g. + * HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields + * are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = lockForWrite(path); + try { dest = file(path.substring(webAppMount.length()), false); if (dest == null) { return false; } - } else { - return false; - } - if (dest.exists() && !overwrite) { - return false; - } + if (dest.exists() && !overwrite) { + return false; + } - try { - if (overwrite) { - Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); - } else { - Files.copy(is, dest.toPath()); + try { + if (overwrite) { + Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } else { + Files.copy(is, dest.toPath()); + } + } catch (IOException ioe) { + return false; } - } catch (IOException ioe) { - return false; - } - return true; + return true; + } finally { + unlockForWrite(lock); + } } @Override @@ -286,6 +318,7 @@ protected void checkType(File file) { @Override protected void initInternal() throws LifecycleException { super.initInternal(); + caseSensitive = isCaseSensitive(); // Is this an exploded web application? if (getWebAppMount().equals("")) { // Look for a manifest @@ -299,4 +332,114 @@ protected void initInternal() throws LifecycleException { } } } + + + /* + * Determines if this ResourceSet is based on a case sensitive file system or not. + */ + private boolean isCaseSensitive() { + try { + String canonicalPath = getFileBase().getCanonicalPath(); + File upper = new File(canonicalPath.toUpperCase(Locale.ENGLISH)); + if (!canonicalPath.equals(upper.getCanonicalPath())) { + return true; + } + File lower = new File(canonicalPath.toLowerCase(Locale.ENGLISH)); + if (!canonicalPath.equals(lower.getCanonicalPath())) { + return true; + } + /* + * Both upper and lower case versions of the current fileBase have the same canonical path so the file + * system must be case insensitive. + */ + } catch (IOException ioe) { + log.warn(sm.getString("dirResourceSet.isCaseSensitive.fail", getFileBase().getAbsolutePath()), ioe); + } + + return false; + } + + + private String getLockKey(String path) { + // Normalize path to ensure that the same key is used for the same path. + String normalisedPath = RequestUtil.normalize(path); + if (caseSensitive) { + return normalisedPath; + } + return normalisedPath.toLowerCase(Locale.ENGLISH); + } + + + @Override + public ResourceLock lockForRead(String path) { + String key = getLockKey(path); + ResourceLock resourceLock = null; + synchronized (resourceLocksByPathLock) { + /* + * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has + * a consistent view of the currently "in-use" ResourceLocks. + */ + resourceLock = resourceLocksByPath.get(key); + if (resourceLock == null) { + resourceLock = new ResourceLock(key); + } + resourceLock.count.incrementAndGet(); + } + // Obtain the lock outside the sync as it will block if there is a current write lock. + resourceLock.reentrantLock.readLock().lock(); + return resourceLock; + } + + + @Override + public void unlockForRead(ResourceLock resourceLock) { + // Unlock outside the sync as there is no need to do it inside. + resourceLock.reentrantLock.readLock().unlock(); + synchronized (resourceLocksByPathLock) { + /* + * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that + * map always has a consistent view of the currently "in-use" ResourceLocks. + */ + if (resourceLock.count.decrementAndGet() == 0) { + resourceLocksByPath.remove(resourceLock.key); + } + } + } + + + @Override + public ResourceLock lockForWrite(String path) { + String key = getLockKey(path); + ResourceLock resourceLock = null; + synchronized (resourceLocksByPathLock) { + /* + * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has + * a consistent view of the currently "in-use" ResourceLocks. + */ + resourceLock = resourceLocksByPath.get(key); + if (resourceLock == null) { + resourceLock = new ResourceLock(key); + } + resourceLock.count.incrementAndGet(); + } + // Obtain the lock outside the sync as it will block if there are any other current locks. + resourceLock.reentrantLock.writeLock().lock(); + return resourceLock; + } + + + @Override + public void unlockForWrite(ResourceLock resourceLock) { + // Unlock outside the sync as there is no need to do it inside. + resourceLock.reentrantLock.writeLock().unlock(); + synchronized (resourceLocksByPathLock) { + /* + * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that + * map always has a consistent view of the currently "in-use" ResourceLocks. + */ + if (resourceLock.count.decrementAndGet() == 0) { + resourceLocksByPath.remove(resourceLock.key); + } + } + } }
java/org/apache/catalina/webresources/FileResource.java+28 −1 modified@@ -31,6 +31,8 @@ import java.security.cert.Certificate; import java.util.jar.Manifest; +import org.apache.catalina.WebResourceLockSet; +import org.apache.catalina.WebResourceLockSet.ResourceLock; import org.apache.catalina.WebResourceRoot; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -62,10 +64,20 @@ public class FileResource extends AbstractResource { private final boolean readOnly; private final Manifest manifest; private final boolean needConvert; + private final WebResourceLockSet lockSet; + private final String lockKey; public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest) { + this(root, webAppPath, resource, readOnly, manifest, null, null); + } + + + public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest, + WebResourceLockSet lockSet, String lockKey) { super(root, webAppPath); this.resource = resource; + this.lockSet = lockSet; + this.lockKey = lockKey; if (webAppPath.charAt(webAppPath.length() - 1) == '/') { String realName = resource.getName() + '/'; @@ -117,7 +129,22 @@ public boolean delete() { if (readOnly) { return false; } - return resource.delete(); + /* + * Lock the path for writing until the delete is complete. The lock prevents concurrent reads and writes (e.g. + * HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields + * are set as if the file exists and some as set as if it does not. + */ + ResourceLock lock = null; + if (lockSet != null) { + lock = lockSet.lockForWrite(lockKey); + } + try { + return resource.delete(); + } finally { + if (lockSet != null) { + lockSet.unlockForWrite(lock); + } + } } @Override
java/org/apache/catalina/webresources/LocalStrings.properties+1 −0 modified@@ -36,6 +36,7 @@ cachedResource.invalidURL=Unable to create an instance of CachedResourceURLStrea classpathUrlStreamHandler.notFound=Unable to load the resource [{0}] using the thread context class loader or the current class''s class loader +dirResourceSet.isCaseSensitive.fail=Error trying to determine if file system at [{0}] is case sensitive so assuming it is not case sensitive dirResourceSet.manifestFail=Failed to read manifest from [{0}] dirResourceSet.notDirectory=The directory specified by base and internal path [{0}]{1}[{2}] does not exist. dirResourceSet.writeNpe=The input stream may not be null
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
16- github.com/advisories/GHSA-5j33-cvvr-w245ghsaADVISORY
- lists.apache.org/thread/y6lj6q1xnp822g6ro70tn19sgtjmr80rghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2024-50379ghsaADVISORY
- www.openwall.com/lists/oss-security/2024/12/17/4ghsaWEB
- www.openwall.com/lists/oss-security/2024/12/18/2ghsaWEB
- github.com/apache/tomcat/commit/05ddeeaa54df1e2dc427d0164bedd6b79f78d81fghsaWEB
- github.com/apache/tomcat/commit/43b507ebac9d268b1ea3d908e296cc6e46795c00ghsaWEB
- github.com/apache/tomcat/commit/631500b0c9b2a2a2abb707e3de2e10a5936e5d41ghsaWEB
- github.com/apache/tomcat/commit/684247ae85fa633b9197b32391de59fc54703842ghsaWEB
- github.com/apache/tomcat/commit/8554f6b1722b33a2ce8b0a3fad37825f3a75f2d2ghsaWEB
- github.com/apache/tomcat/commit/cc7a98b57c6dc1df21979fcff94a36e068f4456cghsaWEB
- lists.debian.org/debian-lts-announce/2025/01/msg00009.htmlghsaWEB
- security.netapp.com/advisory/ntap-20250103-0003ghsaWEB
- tomcat.apache.org/security-10.htmlghsaWEB
- tomcat.apache.org/security-11.htmlghsaWEB
- tomcat.apache.org/security-9.htmlghsaWEB
News mentions
0No linked articles in our index yet.