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

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

commit a5be2adb3a0807aa4d9022ea9439c54914970488
Author: opencode <[email protected]>
AuthorDate: Tue Oct 13 15:18:59 2026 +0200

    Cap the header frame payload buffer expansion at the maximum frame size
    
    Both implementations of HeaderFrameBuffers expanded the payload buffer
    by doubling the current size on every expansion, with no upper bound
    and no overflow guard. For a single header field too large to fit in
    the current buffer this resulted in allocations of one gigabyte or more
    (probable OutOfMemoryError) and, once the doubled size passed
    Integer.MAX_VALUE, an integer overflow to a negative value causing
    ByteBuffer.allocate() to throw IllegalArgumentException. Worse, for
    single fields in the range of 16 MiB to 1 GiB the old code continued
    until the field fit and then wrote the frame with the payload size
    truncated modulo 2^24 by setThreeBytes(), silently corrupting the
    framing on the wire.
    
    Cap the expansion at the maximum frame size that can be represented in
    the frame header (2^24-1, already defined as MAX_MAX_FRAME_SIZE for
    SETTINGS validation). The payload buffer can now never exceed what the
    framing can represent. If the encoded headers still do not fit in a
    buffer of that size, an IllegalStateException with an i18n message is
    thrown between frames. The resulting failure is contained: the stream
    is closed and a GOAWAY is sent, matching the handling of other
    unchecked exceptions on the write path. Behaviour for every buffer size
    below the cap is unchanged. Add a unit test that drives the async
    expansion logic to the cap via reflection and verifies the cap and the
    clean failure beyond it.
---
 .../coyote/http2/Http2AsyncUpgradeHandler.java     | 10 +++-
 .../apache/coyote/http2/Http2UpgradeHandler.java   | 11 +++-
 .../apache/coyote/http2/LocalStrings.properties    |  1 +
 .../coyote/http2/TestHttp2AsyncUpgradeHandler.java | 67 ++++++++++++++++++++++
 4 files changed, 87 insertions(+), 2 deletions(-)

diff --git a/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java 
b/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
index b95fa0f44a..9d75b27f73 100644
--- a/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
+++ b/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
@@ -575,7 +575,15 @@ public class Http2AsyncUpgradeHandler extends 
Http2UpgradeHandler {
 
         @Override
         public void expandPayload() {
-            payloadSize = payloadSize * 2;
+            // The encoded headers are written as the payload of a single
+            // frame so a buffer larger than the largest frame size that can
+            // be represented is never usable. Fail rather than continue
+            // doubling, which would eventually overflow the buffer size.
+            if (payloadSize >= ConnectionSettingsBase.MAX_MAX_FRAME_SIZE) {
+                throw new 
IllegalStateException(sm.getString("upgradeHandler.headerPayloadTooBig",
+                        
Integer.toString(ConnectionSettingsBase.MAX_MAX_FRAME_SIZE)));
+            }
+            payloadSize = Integer.min(payloadSize * 2, 
ConnectionSettingsBase.MAX_MAX_FRAME_SIZE);
             payload = ByteBuffer.allocate(payloadSize);
         }
     }
diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java 
b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
index 124bded377..5ca268b8aa 100644
--- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java
+++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
@@ -2324,7 +2324,16 @@ class Http2UpgradeHandler extends AbstractStream 
implements InternalHttpUpgradeH
 
         @Override
         public void expandPayload() {
-            payload = ByteBuffer.allocate(payload.capacity() * 2);
+            // The encoded headers are written as the payload of a single
+            // frame so a buffer larger than the largest frame size that can
+            // be represented is never usable. Fail rather than continue
+            // doubling, which would eventually overflow the buffer size.
+            if (payload.capacity() >= 
ConnectionSettingsBase.MAX_MAX_FRAME_SIZE) {
+                throw new 
IllegalStateException(sm.getString("upgradeHandler.headerPayloadTooBig",
+                        
Integer.toString(ConnectionSettingsBase.MAX_MAX_FRAME_SIZE)));
+            }
+            payload = ByteBuffer.allocate(
+                    Integer.min(payload.capacity() * 2, 
ConnectionSettingsBase.MAX_MAX_FRAME_SIZE));
         }
     }
 }
diff --git a/java/org/apache/coyote/http2/LocalStrings.properties 
b/java/org/apache/coyote/http2/LocalStrings.properties
index 6fe7897867..4ba4eb677b 100644
--- a/java/org/apache/coyote/http2/LocalStrings.properties
+++ b/java/org/apache/coyote/http2/LocalStrings.properties
@@ -147,6 +147,7 @@ upgradeHandler.enableRfc7540Priorities=Connection [{0}], 
RFC 7540 priorities may
 upgradeHandler.fallToDebug=\n\
 \ Note: further occurrences of HTTP/2 stream errors will be logged at DEBUG 
level.
 upgradeHandler.goaway.debug=Connection [{0}], Goaway, Last stream [{1}], Error 
code [{2}], Debug data [{3}]
+upgradeHandler.headerPayloadTooBig=The encoded headers do not fit in a buffer 
with a size equal to the maximum permitted frame size of [{0}] bytes
 upgradeHandler.init=Connection [{0}], State [{1}]
 upgradeHandler.invalidPreface=Connection [{0}], Invalid connection preface
 upgradeHandler.ioerror=Connection [{0}]
diff --git a/test/org/apache/coyote/http2/TestHttp2AsyncUpgradeHandler.java 
b/test/org/apache/coyote/http2/TestHttp2AsyncUpgradeHandler.java
new file mode 100644
index 0000000000..2043a57a23
--- /dev/null
+++ b/test/org/apache/coyote/http2/TestHttp2AsyncUpgradeHandler.java
@@ -0,0 +1,67 @@
+/*
+ *  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.http2;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestHttp2AsyncUpgradeHandler {
+
+    /*
+     * AsyncHeaderFrameBuffers is a private, static nested class so reflection 
is used to test that payload expansion is
+     * capped at the maximum frame size that can be represented in a frame 
header. Previously the payload size was
+     * simply doubled on every expansion which, for very large header blocks, 
eventually overflowed the size resulting
+     * in an attempt to allocate a buffer with a negative capacity.
+     */
+    @Test
+    public void testPayloadExpansionIsCapped() throws Exception {
+        Class<?> clazz = 
Class.forName("org.apache.coyote.http2.Http2AsyncUpgradeHandler$AsyncHeaderFrameBuffers");
+        Constructor<?> constructor = clazz.getDeclaredConstructor(int.class);
+        constructor.setAccessible(true);
+        Object buffers = constructor.newInstance(Integer.valueOf(1024));
+        Method expandPayload = clazz.getDeclaredMethod("expandPayload");
+        expandPayload.setAccessible(true);
+
+        int expectedSize = 1024;
+        while (expectedSize < ConnectionSettingsBase.MAX_MAX_FRAME_SIZE) {
+            expandPayload.invoke(buffers);
+            expectedSize = Integer.min(expectedSize * 2, 
ConnectionSettingsBase.MAX_MAX_FRAME_SIZE);
+            Assert.assertEquals(expectedSize, readPayloadSize(clazz, buffers));
+        }
+
+        // A buffer that is already as large as the largest permitted frame
+        // can not be expanded further
+        try {
+            expandPayload.invoke(buffers);
+            Assert.fail("Expansion beyond the maximum permitted frame size was 
expected to fail.");
+        } catch (InvocationTargetException e) {
+            Assert.assertEquals(IllegalStateException.class, 
e.getCause().getClass());
+        }
+    }
+
+
+    private int readPayloadSize(Class<?> clazz, Object buffers) throws 
Exception {
+        Field field = clazz.getDeclaredField("payloadSize");
+        field.setAccessible(true);
+        return ((Integer) field.get(buffers)).intValue();
+    }
+}


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

Reply via email to