[Git][java-team/tomcat9][buster] Import Debian changes 9.0.31-1~deb10u13
Markus Koschany (@apo)
gitlab at salsa.debian.org
Tue Jan 28 22:20:42 GMT 2025
Markus Koschany pushed to branch buster at Debian Java Maintainers / tomcat9
Commits:
1b757124 by Markus Koschany at 2025-01-28T23:20:32+01:00
Import Debian changes 9.0.31-1~deb10u13
tomcat9 (9.0.31-1~deb10u13) buster-security; urgency=high
.
* Non-maintainer upload by the ELTS team.
* Fix CVE-2024-52316:
Unchecked Error Condition vulnerability in Apache Tomcat. If Tomcat is
configured to use a custom Jakarta Authentication (formerly JASPIC)
ServerAuthContext component which may throw an exception during the
authentication process without explicitly setting an HTTP status to
indicate failure, the authentication may not fail, allowing the user to
bypass the authentication process. There are no known Jakarta
Authentication components that behave in this way.
* Fix CVE-2024-21733:
Generation of Error Message Containing Sensitive Information vulnerability
in Apache Tomcat.
* Fix CVE-2024-38286:
Apache Tomcat, under certain configurations, allows an attacker to cause an
OutOfMemoryError by abusing the TLS handshake process.
* Fix CVE-2024-50379:
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).
- - - - -
7 changed files:
- debian/changelog
- + debian/patches/CVE-2024-21733.patch
- + debian/patches/CVE-2024-38286.patch
- + debian/patches/CVE-2024-50379-part1.patch
- + debian/patches/CVE-2024-50379-part2.patch
- + debian/patches/CVE-2024-52316.patch
- debian/patches/series
Changes:
=====================================
debian/changelog
=====================================
@@ -1,3 +1,28 @@
+tomcat9 (9.0.31-1~deb10u13) buster-security; urgency=high
+
+ * Non-maintainer upload by the ELTS team.
+ * Fix CVE-2024-52316:
+ Unchecked Error Condition vulnerability in Apache Tomcat. If Tomcat is
+ configured to use a custom Jakarta Authentication (formerly JASPIC)
+ ServerAuthContext component which may throw an exception during the
+ authentication process without explicitly setting an HTTP status to
+ indicate failure, the authentication may not fail, allowing the user to
+ bypass the authentication process. There are no known Jakarta
+ Authentication components that behave in this way.
+ * Fix CVE-2024-21733:
+ Generation of Error Message Containing Sensitive Information vulnerability
+ in Apache Tomcat.
+ * Fix CVE-2024-38286:
+ Apache Tomcat, under certain configurations, allows an attacker to cause an
+ OutOfMemoryError by abusing the TLS handshake process.
+ * Fix CVE-2024-50379:
+ 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).
+
+ -- Markus Koschany <apo at debian.org> Wed, 15 Jan 2025 04:13:35 +0100
+
tomcat9 (9.0.31-1~deb10u12) buster-security; urgency=high
* Team upload.
=====================================
debian/patches/CVE-2024-21733.patch
=====================================
@@ -0,0 +1,245 @@
+From: Markus Koschany <apo at debian.org>
+Date: Sun, 18 Aug 2024 20:01:54 +0200
+Subject: CVE-2024-21733
+
+Origin: https://github.com/apache/tomcat/commit/86ccc43940861703c2be96a5f35384407522125a
+---
+ .../apache/coyote/http11/Http11InputBuffer.java | 28 +--
+ .../apache/catalina/core/TestAsyncContextImpl.java | 170 +++++++++++++++++-
+ .../catalina/nonblocking/TestNonBlockingAPI.java | 192 +++++++++++++++++++++
+ 3 files changed, 378 insertions(+), 12 deletions(-)
+
+diff --git a/java/org/apache/coyote/http11/Http11InputBuffer.java b/java/org/apache/coyote/http11/Http11InputBuffer.java
+index 00779ed..44a1b37 100644
+--- a/java/org/apache/coyote/http11/Http11InputBuffer.java
++++ b/java/org/apache/coyote/http11/Http11InputBuffer.java
+@@ -725,19 +725,25 @@ public class Http11InputBuffer implements InputBuffer, ApplicationBufferHandler
+ byteBuffer.limit(end).position(end);
+ }
+
+- byteBuffer.mark();
+- if (byteBuffer.position() < byteBuffer.limit()) {
+- byteBuffer.position(byteBuffer.limit());
+- }
+- byteBuffer.limit(byteBuffer.capacity());
+- SocketWrapperBase<?> socketWrapper = this.wrapper;
+ int nRead = -1;
+- if (socketWrapper != null) {
+- nRead = socketWrapper.read(block, byteBuffer);
+- } else {
+- throw new CloseNowException(sm.getString("iib.eof.error"));
++ byteBuffer.mark();
++ try {
++ if (byteBuffer.position() < byteBuffer.limit()) {
++ byteBuffer.position(byteBuffer.limit());
++ }
++ byteBuffer.limit(byteBuffer.capacity());
++ SocketWrapperBase<?> socketWrapper = this.wrapper;
++ if (socketWrapper != null) {
++ nRead = socketWrapper.read(block, byteBuffer);
++ } else {
++ throw new CloseNowException(sm.getString("iib.eof.error"));
++ }
++ } finally {
++ // Ensure that the buffer limit and position are returned to a
++ // consistent "ready for read" state if an error occurs during in
++ // the above code block.
++ byteBuffer.limit(byteBuffer.position()).reset();
+ }
+- byteBuffer.limit(byteBuffer.position()).reset();
+ if (nRead > 0) {
+ return true;
+ } else if (nRead == -1) {
+diff --git a/test/org/apache/catalina/core/TestAsyncContextImpl.java b/test/org/apache/catalina/core/TestAsyncContextImpl.java
+index e4a9b63..0a68ebf 100644
+--- a/test/org/apache/catalina/core/TestAsyncContextImpl.java
++++ b/test/org/apache/catalina/core/TestAsyncContextImpl.java
+@@ -17,6 +17,7 @@
+ package org.apache.catalina.core;
+
+ import java.io.IOException;
++import java.io.InputStream;
+ import java.io.PrintWriter;
+ import java.net.URI;
+ import java.net.URISyntaxException;
+@@ -842,7 +843,7 @@ public class TestAsyncContextImpl extends TomcatBaseTest {
+ }
+ }
+
+- private static class TrackingListener implements AsyncListener {
++ public static class TrackingListener implements AsyncListener {
+
+ private final boolean completeOnError;
+ private final boolean completeOnTimeout;
+@@ -3001,4 +3002,171 @@ public class TestAsyncContextImpl extends TomcatBaseTest {
+ }
+ }
+
++
++ /*
++ * Tests an error on an async thread when the client closes the connection
++ * before fully writing the request body.
++ *
++ * Required sequence is:
++ * - enter Servlet's service() method
++ * - startAsync()
++ * - start async thread
++ * - read partial body
++ * - close client connection
++ * - read on async thread -> I/O error
++ * - exit Servlet's service() method
++ *
++ * This test makes extensive use of instance fields in the Servlet that
++ * would normally be considered very poor practice. It is only safe in this
++ * test as the Servlet only processes a single request.
++ */
++ @Test
++ public void testCanceledPost() throws Exception {
++ CountDownLatch partialReadLatch = new CountDownLatch(1);
++ CountDownLatch clientCloseLatch = new CountDownLatch(1);
++ CountDownLatch threadCompleteLatch = new CountDownLatch(1);
++
++ AtomicBoolean testFailed = new AtomicBoolean(true);
++
++ // Setup Tomcat instance
++ Tomcat tomcat = getTomcatInstance();
++
++ // No file system docBase required
++ Context ctx = tomcat.addContext("", null);
++
++ PostServlet postServlet = new PostServlet(partialReadLatch, clientCloseLatch, threadCompleteLatch, testFailed);
++ Wrapper wrapper = Tomcat.addServlet(ctx, "postServlet", postServlet);
++ wrapper.setAsyncSupported(true);
++ ctx.addServletMappingDecoded("/*", "postServlet");
++
++ tomcat.start();
++
++ PostClient client = new PostClient();
++ client.setPort(getPort());
++ client.setRequest(new String[] { "POST / HTTP/1.1" + SimpleHttpClient.CRLF +
++ "Host: localhost:" + SimpleHttpClient.CRLF +
++ "Content-Length: 100" + SimpleHttpClient.CRLF +
++ SimpleHttpClient.CRLF +
++ "This is 16 bytes"
++ });
++ client.connect();
++ client.sendRequest();
++
++ // Wait server to read partial request body
++ partialReadLatch.await();
++
++ client.disconnect();
++
++ clientCloseLatch.countDown();
++
++ threadCompleteLatch.await();
++
++ Assert.assertFalse(testFailed.get());
++ }
++
++
++ private static final class PostClient extends SimpleHttpClient {
++
++ @Override
++ public boolean isResponseBodyOK() {
++ return true;
++ }
++ }
++
++
++ private static final class PostServlet extends HttpServlet {
++
++ private static final long serialVersionUID = 1L;
++
++ private final transient CountDownLatch partialReadLatch;
++ private final transient CountDownLatch clientCloseLatch;
++ private final transient CountDownLatch threadCompleteLatch;
++ private final AtomicBoolean testFailed;
++
++ public PostServlet(CountDownLatch doPostLatch, CountDownLatch clientCloseLatch,
++ CountDownLatch threadCompleteLatch, AtomicBoolean testFailed) {
++ this.partialReadLatch = doPostLatch;
++ this.clientCloseLatch = clientCloseLatch;
++ this.threadCompleteLatch = threadCompleteLatch;
++ this.testFailed = testFailed;
++ }
++
++ @Override
++ protected void doPost(HttpServletRequest req, HttpServletResponse resp)
++ throws ServletException, IOException {
++
++ AsyncContext ac = req.startAsync();
++ Thread t = new PostServletThread(ac, partialReadLatch, clientCloseLatch, threadCompleteLatch, testFailed);
++ t.start();
++
++ try {
++ threadCompleteLatch.await();
++ } catch (InterruptedException e) {
++ // Ignore
++ }
++ }
++ }
++
++
++ private static final class PostServletThread extends Thread {
++
++ private final AsyncContext ac;
++ private final CountDownLatch partialReadLatch;
++ private final CountDownLatch clientCloseLatch;
++ private final CountDownLatch threadCompleteLatch;
++ private final AtomicBoolean testFailed;
++
++ public PostServletThread(AsyncContext ac, CountDownLatch partialReadLatch, CountDownLatch clientCloseLatch,
++ CountDownLatch threadCompleteLatch, AtomicBoolean testFailed) {
++ this.ac = ac;
++ this.partialReadLatch = partialReadLatch;
++ this.clientCloseLatch = clientCloseLatch;
++ this.threadCompleteLatch = threadCompleteLatch;
++ this.testFailed = testFailed;
++ }
++
++ @Override
++ public void run() {
++ try {
++ int bytesRead = 0;
++ byte[] buffer = new byte[32];
++ InputStream is = null;
++
++ try {
++ is = ac.getRequest().getInputStream();
++
++ // Read the partial request body
++ while (bytesRead < 16) {
++ int read = is.read(buffer);
++ if (read == -1) {
++ // Error condition
++ return;
++ }
++ bytesRead += read;
++ }
++ } catch (IOException ioe) {
++ // Error condition
++ return;
++ } finally {
++ partialReadLatch.countDown();
++ }
++
++ // Wait for client to close connection
++ clientCloseLatch.await();
++
++ // Read again
++ try {
++ is.read();
++ } catch (IOException e) {
++ e.printStackTrace();
++ // Required. Clear the error marker.
++ testFailed.set(false);
++ }
++ } catch (InterruptedException e) {
++ // Ignore
++ } finally {
++ threadCompleteLatch.countDown();
++ }
++ }
++ }
+ }
=====================================
debian/patches/CVE-2024-38286.patch
=====================================
@@ -0,0 +1,163 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 8 Jan 2025 13:16:41 +0100
+Subject: CVE-2024-38286
+
+Origin: https://github.com/apache/tomcat/commit/76c5cce6f0bcef14b0c21c38910371ca7d322d13
+---
+ .../apache/tomcat/util/net/LocalStrings.properties | 2 ++
+ .../apache/tomcat/util/net/SecureNio2Channel.java | 28 +++++++++++++++++++---
+ .../apache/tomcat/util/net/SecureNioChannel.java | 19 +++++++++++++++
+ 3 files changed, 46 insertions(+), 3 deletions(-)
+
+diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties b/java/org/apache/tomcat/util/net/LocalStrings.properties
+index 0fdd2df..2cf8904 100644
+--- a/java/org/apache/tomcat/util/net/LocalStrings.properties
++++ b/java/org/apache/tomcat/util/net/LocalStrings.properties
+@@ -23,6 +23,8 @@ channel.nio.ssl.expandNetInBuffer=Expanding network input buffer to [{0}] bytes
+ channel.nio.ssl.expandNetOutBuffer=Expanding network output buffer to [{0}] bytes
+ channel.nio.ssl.foundHttp=Found an plain text HTTP request on what should be an encrypted TLS connection
+ channel.nio.ssl.handshakeError=Handshake error
++channel.nio.ssl.handshakeWrapPending=There is already handshake data waiting to be wrapped
++channel.nio.ssl.handshakeWrapQueueTooLong=The queue of handshake data to be wrapped has grown too long
+ channel.nio.ssl.incompleteHandshake=Handshake incomplete, you must complete handshake before reading data.
+ channel.nio.ssl.invalidCloseState=Invalid close state, will not send network data.
+ channel.nio.ssl.invalidStatus=Unexpected status [{0}].
+diff --git a/java/org/apache/tomcat/util/net/SecureNio2Channel.java b/java/org/apache/tomcat/util/net/SecureNio2Channel.java
+index 54d9e03..6648cc5 100644
+--- a/java/org/apache/tomcat/util/net/SecureNio2Channel.java
++++ b/java/org/apache/tomcat/util/net/SecureNio2Channel.java
+@@ -51,10 +51,12 @@ public class SecureNio2Channel extends Nio2Channel {
+ private static final Log log = LogFactory.getLog(SecureNio2Channel.class);
+ private static final StringManager sm = StringManager.getManager(SecureNio2Channel.class);
+
+- // Value determined by observation of what the SSL Engine requested in
+- // various scenarios
++ // Value determined by observation of what the SSL Engine requested in various scenarios
+ private static final int DEFAULT_NET_BUFFER_SIZE = 16921;
+
++ // Much longer than it should ever need to be but short enough to trigger connection closure if something goes wrong
++ private static final int HANDSHAKE_WRAP_QUEUE_LENGTH_LIMIT = 100;
++
+ protected final Nio2Endpoint endpoint;
+
+ protected ByteBuffer netInBuffer;
+@@ -65,6 +67,7 @@ public class SecureNio2Channel extends Nio2Channel {
+ protected boolean sniComplete = false;
+
+ private volatile boolean handshakeComplete = false;
++ private volatile int handshakeWrapQueueLength = 0;
+ private volatile HandshakeStatus handshakeStatus; //gets set by handshake
+
+ protected boolean closed;
+@@ -726,6 +729,11 @@ public class SecureNio2Channel extends Nio2Channel {
+ //perform any tasks if needed
+ if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ tasks();
++ } else if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
++ if (++handshakeWrapQueueLength > HANDSHAKE_WRAP_QUEUE_LENGTH_LIMIT) {
++ throw new ExecutionException(
++ new IOException(sm.getString("channel.nio.ssl.handshakeWrapQueueTooLong")));
++ }
+ }
+ //if we need more network data, then bail out for now.
+ if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) {
+@@ -856,6 +864,8 @@ public class SecureNio2Channel extends Nio2Channel {
+ if (!netOutBuffer.hasRemaining()) {
+ netOutBuffer.clear();
+ SSLEngineResult result = sslEngine.wrap(src, netOutBuffer);
++ // Call to wrap() will have included any required handshake data
++ handshakeWrapQueueLength = 0;
+ written = result.bytesConsumed();
+ netOutBuffer.flip();
+ if (result.getStatus() == Status.OK) {
+@@ -920,6 +930,11 @@ public class SecureNio2Channel extends Nio2Channel {
+ //perform any tasks if needed
+ if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK)
+ tasks();
++ } else if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
++ if (++handshakeWrapQueueLength > HANDSHAKE_WRAP_QUEUE_LENGTH_LIMIT) {
++ throw new ExecutionException(new IOException(
++ sm.getString("channel.nio.ssl.handshakeWrapQueueTooLong")));
++ }
+ //if we need more network data, then bail out for now.
+ if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) {
+ if (read == 0) {
+@@ -1034,6 +1049,11 @@ public class SecureNio2Channel extends Nio2Channel {
+ //perform any tasks if needed
+ if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK)
+ tasks();
++ } else if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
++ if (++handshakeWrapQueueLength > HANDSHAKE_WRAP_QUEUE_LENGTH_LIMIT) {
++ throw new ExecutionException(new IOException(
++ sm.getString("channel.nio.ssl.handshakeWrapQueueTooLong")));
++ }
+ //if we need more network data, then bail out for now.
+ if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) {
+ if (read == 0) {
+@@ -1141,6 +1161,8 @@ public class SecureNio2Channel extends Nio2Channel {
+ netOutBuffer.clear();
+ // Wrap the source data into the internal buffer
+ SSLEngineResult result = sslEngine.wrap(src, netOutBuffer);
++ // Call to wrap() will have included any required handshake data
++ handshakeWrapQueueLength = 0;
+ final int written = result.bytesConsumed();
+ netOutBuffer.flip();
+ if (result.getStatus() == Status.OK) {
+@@ -1248,4 +1270,4 @@ public class SecureNio2Channel extends Nio2Channel {
+ public ByteBuffer getEmptyBuf() {
+ return emptyBuf;
+ }
+-}
+\ No newline at end of file
++}
+diff --git a/java/org/apache/tomcat/util/net/SecureNioChannel.java b/java/org/apache/tomcat/util/net/SecureNioChannel.java
+index 7d128fb..5076c1f 100644
+--- a/java/org/apache/tomcat/util/net/SecureNioChannel.java
++++ b/java/org/apache/tomcat/util/net/SecureNioChannel.java
+@@ -63,6 +63,7 @@ public class SecureNioChannel extends NioChannel {
+ protected boolean sniComplete = false;
+
+ protected boolean handshakeComplete = false;
++ protected boolean needHandshakeWrap = false;
+ protected HandshakeStatus handshakeStatus; //gets set by handshake
+
+ protected boolean closed = false;
+@@ -624,6 +625,14 @@ public class SecureNioChannel extends NioChannel {
+ //perform any tasks if needed
+ if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ tasks();
++ } else if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
++ if (getOutboundRemaining() == 0) {
++ handshakeWrap(true);
++ } else if (needHandshakeWrap) {
++ throw new IOException(sm.getString("channel.nio.ssl.handshakeWrapPending"));
++ } else {
++ needHandshakeWrap = true;
++ }
+ }
+ //if we need more network data, then bail out for now.
+ if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) {
+@@ -707,6 +716,14 @@ public class SecureNioChannel extends NioChannel {
+ //perform any tasks if needed
+ if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ tasks();
++ } else if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
++ if (getOutboundRemaining() == 0) {
++ handshakeWrap(true);
++ } else if (needHandshakeWrap) {
++ throw new IOException(sm.getString("channel.nio.ssl.handshakeWrapPending"));
++ } else {
++ needHandshakeWrap = true;
++ }
+ }
+ //if we need more network data, then bail out for now.
+ if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) {
+@@ -801,6 +818,8 @@ public class SecureNioChannel extends NioChannel {
+ netOutBuffer.clear();
+
+ SSLEngineResult result = sslEngine.wrap(src, netOutBuffer);
++ // Call to wrap() will have included any required handshake data
++ needHandshakeWrap = false;
+ // The number of bytes written
+ int written = result.bytesConsumed();
+ netOutBuffer.flip();
=====================================
debian/patches/CVE-2024-50379-part1.patch
=====================================
@@ -0,0 +1,429 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 8 Jan 2025 14:39:47 +0100
+Subject: CVE-2024-50379 part1
+
+Origin: https://github.com/apache/tomcat/commit/43b507ebac9d268b1ea3d908e296cc6e46795c00
+---
+ java/org/apache/catalina/WebResourceLockSet.java | 73 ++++++++
+ .../catalina/webresources/DirResourceSet.java | 199 ++++++++++++++++++---
+ .../apache/catalina/webresources/FileResource.java | 27 ++-
+ .../catalina/webresources/LocalStrings.properties | 1 +
+ 4 files changed, 271 insertions(+), 29 deletions(-)
+ create mode 100644 java/org/apache/catalina/WebResourceLockSet.java
+
+diff --git a/java/org/apache/catalina/WebResourceLockSet.java b/java/org/apache/catalina/WebResourceLockSet.java
+new file mode 100644
+index 0000000..142472c
+--- /dev/null
++++ b/java/org/apache/catalina/WebResourceLockSet.java
+@@ -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;
++ }
++ }
++}
+diff --git a/java/org/apache/catalina/webresources/DirResourceSet.java b/java/org/apache/catalina/webresources/DirResourceSet.java
+index 234dc74..a85e648 100644
+--- a/java/org/apache/catalina/webresources/DirResourceSet.java
++++ b/java/org/apache/catalina/webresources/DirResourceSet.java
+@@ -22,24 +22,35 @@ import java.io.IOException;
+ 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.
+ */
+@@ -98,22 +109,33 @@ public class DirResourceSet extends AbstractFileResourceSet {
+ 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);
+@@ -223,32 +245,42 @@ public class DirResourceSet extends AbstractFileResourceSet {
+ 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
+@@ -263,6 +295,7 @@ public class DirResourceSet extends AbstractFileResourceSet {
+ @Override
+ protected void initInternal() throws LifecycleException {
+ super.initInternal();
++ caseSensitive = isCaseSensitive();
+ // Is this an exploded web application?
+ if (getWebAppMount().equals("")) {
+ // Look for a manifest
+@@ -276,4 +309,114 @@ public class DirResourceSet extends AbstractFileResourceSet {
+ }
+ }
+ }
++
++
++ /*
++ * 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);
++ }
++ }
++ }
+ }
+diff --git a/java/org/apache/catalina/webresources/FileResource.java b/java/org/apache/catalina/webresources/FileResource.java
+index babe190..54fc23b 100644
+--- a/java/org/apache/catalina/webresources/FileResource.java
++++ b/java/org/apache/catalina/webresources/FileResource.java
+@@ -30,6 +30,8 @@ import java.nio.file.attribute.BasicFileAttributes;
+ 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,11 +64,19 @@ 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() + '/';
+@@ -120,7 +130,22 @@ public class FileResource extends AbstractResource {
+ 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
+diff --git a/java/org/apache/catalina/webresources/LocalStrings.properties b/java/org/apache/catalina/webresources/LocalStrings.properties
+index fb9badc..b4fc0d2 100644
+--- a/java/org/apache/catalina/webresources/LocalStrings.properties
++++ b/java/org/apache/catalina/webresources/LocalStrings.properties
+@@ -29,6 +29,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
=====================================
debian/patches/CVE-2024-50379-part2.patch
=====================================
@@ -0,0 +1,29 @@
+From: Markus Koschany <apo at debian.org>
+Date: Wed, 8 Jan 2025 14:49:01 +0100
+Subject: CVE-2024-50379 part2
+
+Origin: https://github.com/apache/tomcat/commit/631500b0c9b2a2a2abb707e3de2e10a5936e5d41
+---
+ java/org/apache/catalina/webresources/DirResourceSet.java | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/java/org/apache/catalina/webresources/DirResourceSet.java b/java/org/apache/catalina/webresources/DirResourceSet.java
+index a85e648..ce7ba34 100644
+--- a/java/org/apache/catalina/webresources/DirResourceSet.java
++++ b/java/org/apache/catalina/webresources/DirResourceSet.java
+@@ -359,6 +359,7 @@ public class DirResourceSet extends AbstractFileResourceSet implements WebResour
+ resourceLock = resourceLocksByPath.get(key);
+ if (resourceLock == null) {
+ resourceLock = new ResourceLock(key);
++ resourceLocksByPath.put(key, resourceLock);
+ }
+ resourceLock.count.incrementAndGet();
+ }
+@@ -396,6 +397,7 @@ public class DirResourceSet extends AbstractFileResourceSet implements WebResour
+ resourceLock = resourceLocksByPath.get(key);
+ if (resourceLock == null) {
+ resourceLock = new ResourceLock(key);
++ resourceLocksByPath.put(key, resourceLock);
+ }
+ resourceLock.count.incrementAndGet();
+ }
=====================================
debian/patches/CVE-2024-52316.patch
=====================================
@@ -0,0 +1,22 @@
+From: Markus Koschany <apo at debian.org>
+Date: Tue, 14 Jan 2025 19:00:00 +0100
+Subject: CVE-2024-52316
+
+Origin: https://github.com/apache/tomcat/commit/7532f9dc4a8c37ec958f79dc82c4924a6c539223
+---
+ java/org/apache/catalina/authenticator/AuthenticatorBase.java | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/java/org/apache/catalina/authenticator/AuthenticatorBase.java b/java/org/apache/catalina/authenticator/AuthenticatorBase.java
+index 208155f..e588b8c 100644
+--- a/java/org/apache/catalina/authenticator/AuthenticatorBase.java
++++ b/java/org/apache/catalina/authenticator/AuthenticatorBase.java
+@@ -906,6 +906,8 @@ public abstract class AuthenticatorBase extends ValveBase
+ authStatus = state.serverAuthContext.validateRequest(state.messageInfo, client, null);
+ } catch (AuthException e) {
+ log.debug(sm.getString("authenticator.loginFail"), e);
++ // Need to explicitly set the return code as the ServerAuthContext may not have done.
++ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ return false;
+ }
+
=====================================
debian/patches/series
=====================================
@@ -40,3 +40,8 @@ CVE-2023-45648.patch
0040-2-2-CVE-2023-46589-Ensure-IOException-on-request-rea.patch
CVE-2024-23672.patch
CVE-2024-24549.patch
+CVE-2024-21733.patch
+CVE-2024-38286.patch
+CVE-2024-50379-part1.patch
+CVE-2024-50379-part2.patch
+CVE-2024-52316.patch
View it on GitLab: https://salsa.debian.org/java-team/tomcat9/-/commit/1b757124a6177b40bd48c9416b2e806748db2298
--
View it on GitLab: https://salsa.debian.org/java-team/tomcat9/-/commit/1b757124a6177b40bd48c9416b2e806748db2298
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20250128/40c02e71/attachment.htm>
More information about the pkg-java-commits
mailing list