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

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

commit 9b9016ee5e10057b92f522d57c81dc696cb7eb9d
Author: opencode <[email protected]>
AuthorDate: Tue Oct 13 09:35:19 2026 +0200

    Report end of stream from the buffered input filter
    
    The buffered input filter reports the length of the buffered body from
    available() without consulting whether that body has already been
    delivered. Once doRead() had handed the whole body to the caller,
    further available() calls continued to report the delivered body length.
    When there was no body at all, available() fell through to the
    downstream buffer, whose data belongs to the TLS renegotiation or to a
    pipelined next request rather than to the body of this request.
    
    The connector converts any non-zero value reported by the input buffer
    chain into a single available byte, so an application polling
    ServletInputStream.available() after the end of the stream was reached
    saw a contract-violating non-zero value, and a loop of the form
    while (available() > 0) that did not check the read result never
    terminated.
    
    Report zero bytes available once the filter has finished, consistent
    with the filter's doRead() and isFinished(). This is safe because the
    whole body is buffered synchronously by setRequest() when the filter is
    activated, before the application can read or query availability, so
    available() can never observe a partially buffered body.
---
 .../coyote/http11/filters/BufferedInputFilter.java |  15 ++-
 .../http11/filters/TestBufferedInputFilter.java    | 148 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   9 ++
 3 files changed, 166 insertions(+), 6 deletions(-)

diff --git a/java/org/apache/coyote/http11/filters/BufferedInputFilter.java 
b/java/org/apache/coyote/http11/filters/BufferedInputFilter.java
index 878ca7487b..fee0d5c113 100644
--- a/java/org/apache/coyote/http11/filters/BufferedInputFilter.java
+++ b/java/org/apache/coyote/http11/filters/BufferedInputFilter.java
@@ -159,13 +159,16 @@ public class BufferedInputFilter implements InputFilter, 
ApplicationBufferHandle
 
     @Override
     public int available() {
-        int available = buffered.getLength();
-        if (available == 0) {
-            // No data buffered here. Try the next filter in the chain.
-            return buffer.available();
-        } else {
-            return available;
+        if (isFinished()) {
+            // The whole body has been delivered to the caller or there was no
+            // body. Nothing is available from this filter and the next filter
+            // in the chain must not be consulted since any data there does not
+            // belong to the body.
+            return 0;
         }
+        // The whole body is buffered at this point, so its length is the
+        // number of bytes that remain to be delivered.
+        return buffered.getLength();
     }
 
 
diff --git a/test/org/apache/coyote/http11/filters/TestBufferedInputFilter.java 
b/test/org/apache/coyote/http11/filters/TestBufferedInputFilter.java
new file mode 100644
index 0000000000..03954c5dfb
--- /dev/null
+++ b/test/org/apache/coyote/http11/filters/TestBufferedInputFilter.java
@@ -0,0 +1,148 @@
+/*
+ *  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.coyote.http11.filters;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.coyote.InputBuffer;
+import org.apache.coyote.Request;
+import org.apache.tomcat.util.net.ApplicationBufferHandler;
+
+public class TestBufferedInputFilter {
+
+    /*
+     * The filter must report the buffered body length as available until the
+     * body has been delivered to the caller and must then report no data as
+     * available rather than continuing to report the delivered body length.
+     */
+    @Test
+    public void testAvailableAfterBodyDelivered() throws IOException {
+        byte[] body = "0123456789".getBytes(StandardCharsets.ISO_8859_1);
+
+        BufferedInputFilter filter = new BufferedInputFilter(-1);
+        filter.setLimit(1024);
+        filter.setBuffer(new FixedSizeBodySource(body, 4, 0));
+
+        // Buffer the body, as Http11InputBuffer.addActiveFilter() does
+        filter.setRequest(new Request());
+
+        Assert.assertFalse(filter.isFinished());
+        Assert.assertEquals(body.length, filter.available());
+
+        // Deliver the body to the caller
+        DeliveryTarget target = new DeliveryTarget();
+        Assert.assertEquals(body.length, filter.doRead(target));
+        Assert.assertArrayEquals(body, target.getBytes());
+
+        // The body has been delivered. Further reads and available() must
+        // report the end of the body. available() is expected to keep
+        // reporting zero when called repeatedly.
+        Assert.assertTrue(filter.isFinished());
+        Assert.assertEquals(0, filter.available());
+        Assert.assertEquals(0, filter.available());
+        Assert.assertEquals(-1, filter.doRead(target));
+    }
+
+
+    /*
+     * A filter with an empty body must report no data as available rather
+     * than falling through to the downstream buffer whose data does not
+     * belong to the body of this request.
+     */
+    @Test
+    public void testAvailableWithEmptyBody() throws IOException {
+        BufferedInputFilter filter = new BufferedInputFilter(-1);
+        filter.setLimit(1024);
+        // The body is empty but the downstream buffer has data, for example
+        // from a pipelined next request
+        filter.setBuffer(new FixedSizeBodySource(new byte[0], 4, 10));
+
+        // Buffer the body, as Http11InputBuffer.addActiveFilter() does
+        filter.setRequest(new Request());
+
+        Assert.assertTrue(filter.isFinished());
+        Assert.assertEquals(0, filter.available());
+    }
+
+
+    /*
+     * Feeds the given body to the filter in fixed size chunks followed by
+     * EOF. The value the available() method reports after the body has been
+     * drained can be configured.
+     */
+    private static class FixedSizeBodySource implements InputBuffer {
+
+        private final ByteBuffer source;
+        private final int chunkSize;
+        private final int availableAfterDrain;
+
+        FixedSizeBodySource(byte[] body, int chunkSize, int 
availableAfterDrain) {
+            source = ByteBuffer.wrap(body);
+            this.chunkSize = chunkSize;
+            this.availableAfterDrain = availableAfterDrain;
+        }
+
+        @Override
+        public int doRead(ApplicationBufferHandler handler) throws IOException 
{
+            if (!source.hasRemaining()) {
+                return -1;
+            }
+            int len = Math.min(chunkSize, source.remaining());
+            byte[] bytes = new byte[len];
+            source.get(bytes);
+            handler.setByteBuffer(ByteBuffer.wrap(bytes));
+            return len;
+        }
+
+        @Override
+        public int available() {
+            return source.remaining() > 0 ? source.remaining() : 
availableAfterDrain;
+        }
+    }
+
+
+    private static class DeliveryTarget implements ApplicationBufferHandler {
+
+        private ByteBuffer buffer;
+
+        @Override
+        public void setByteBuffer(ByteBuffer buffer) {
+            this.buffer = buffer;
+        }
+
+        @Override
+        public ByteBuffer getByteBuffer() {
+            return buffer;
+        }
+
+        @Override
+        public void expand(int size) {
+            // The buffer is never expanded
+        }
+
+        byte[] getBytes() {
+            byte[] bytes = new byte[buffer.remaining()];
+            buffer.get(bytes);
+            return bytes;
+        }
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index e23c121673..a05b9fd255 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -439,6 +439,15 @@
         a null pointer exception instead of the expected 501 response.
         (remm)
       </fix>
+      <fix>
+        Stop reporting available bytes once a request body that was buffered
+        to allow a TLS client authentication renegotiation has been read in
+        full. Reporting the buffered length, or data belonging to a pipelined
+        next request, as available after the end of the stream was reached
+        violated the <code>ServletInputStream</code> contract and could keep
+        an application loop that polls <code>available()</code> running.
+        (remm)
+      </fix>
       <!-- Entries for backport and removal before 12.0.0-M1 below this line 
-->
       <fix>
         Keep the asynchronous timeout in place until the timeout event has


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

Reply via email to