This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c8034ba400680ef49c1887df7da93472543d2aac Author: Mark Thomas <ma...@apache.org> AuthorDate: Mon Mar 15 17:57:41 2021 +0000 Fix BZ 65179 - avoid flow control window exhaustion Ensure that the connection level flow control window from the client to the server is updated when handling DATA frames received for completed streams else the flow control window may become exhausted https://bz.apache.org/bugzilla/show_bug.cgi?id=65179 --- .../apache/coyote/http2/AbstractNonZeroStream.java | 16 +++ java/org/apache/coyote/http2/Http2Parser.java | 82 +++++++------ .../apache/coyote/http2/Http2UpgradeHandler.java | 22 ++-- java/org/apache/coyote/http2/RecycledStream.java | 30 ++++- java/org/apache/coyote/http2/Stream.java | 42 +++++-- test/org/apache/coyote/http2/Http2TestBase.java | 7 +- test/org/apache/coyote/http2/TestFlowControl.java | 131 +++++++++++++++++++++ webapps/docs/changelog.xml | 6 + 8 files changed, 271 insertions(+), 65 deletions(-) diff --git a/java/org/apache/coyote/http2/AbstractNonZeroStream.java b/java/org/apache/coyote/http2/AbstractNonZeroStream.java index 38761ad..0368c4f 100644 --- a/java/org/apache/coyote/http2/AbstractNonZeroStream.java +++ b/java/org/apache/coyote/http2/AbstractNonZeroStream.java @@ -16,6 +16,7 @@ */ package org.apache.coyote.http2; +import java.nio.ByteBuffer; import java.util.Iterator; import org.apache.juli.logging.Log; @@ -31,6 +32,8 @@ abstract class AbstractNonZeroStream extends AbstractStream { private static final Log log = LogFactory.getLog(AbstractNonZeroStream.class); private static final StringManager sm = StringManager.getManager(AbstractNonZeroStream.class); + protected static final ByteBuffer ZERO_LENGTH_BYTEBUFFER = ByteBuffer.allocate(0); + protected final StreamStateMachine state; private volatile int weight = Constants.DEFAULT_WEIGHT; @@ -140,4 +143,17 @@ abstract class AbstractNonZeroStream extends AbstractStream { final void checkState(FrameType frameType) throws Http2Exception { state.checkFrameType(frameType); } + + + /** + * Obtain the ByteBuffer to store DATA frame payload data for this stream + * that has been received from the client. + * + * @return {@code null} if the DATA frame payload can be swallowed, or a + * ByteBuffer with at least enough space remaining for the current + * flow control window for stream data from the client. + */ + abstract ByteBuffer getInputByteBuffer(); + + abstract void receivedData(int payloadSize) throws Http2Exception; } diff --git a/java/org/apache/coyote/http2/Http2Parser.java b/java/org/apache/coyote/http2/Http2Parser.java index cad0fd4..0550496 100644 --- a/java/org/apache/coyote/http2/Http2Parser.java +++ b/java/org/apache/coyote/http2/Http2Parser.java @@ -175,7 +175,7 @@ class Http2Parser { swallowPayload(streamId, FrameType.DATA.getId(), dataLength, false, buffer); // Process padding before sending any notifications in case padding // is invalid. - if (padLength > 0) { + if (Flags.hasPadding(flags)) { swallowPayload(streamId, FrameType.DATA.getId(), padLength, true, buffer); } if (endOfStream) { @@ -183,9 +183,12 @@ class Http2Parser { } } else { synchronized (dest) { - if (dest.remaining() < dataLength) { - swallowPayload(streamId, FrameType.DATA.getId(), dataLength, false, buffer); + if (dest.remaining() < payloadSize) { // Client has sent more data than permitted by Window size + swallowPayload(streamId, FrameType.DATA.getId(), dataLength, false, buffer); + if (Flags.hasPadding(flags)) { + swallowPayload(streamId, FrameType.DATA.getId(), padLength, true, buffer); + } throw new StreamException(sm.getString("http2Parser.processFrameData.window", connectionId), Http2Error.FLOW_CONTROL_ERROR, streamId); } @@ -199,7 +202,7 @@ class Http2Parser { } // Process padding before sending any notifications in case // padding is invalid. - if (padLength > 0) { + if (Flags.hasPadding(flags)) { swallowPayload(streamId, FrameType.DATA.getId(), padLength, true, buffer); } if (endOfStream) { @@ -208,9 +211,6 @@ class Http2Parser { output.endRequestBodyFrame(streamId); } } - if (Flags.hasPadding(flags)) { - output.onSwallowedDataFramePayload(streamId, padLength); - } } @@ -521,10 +521,11 @@ class Http2Parser { try { swallowPayload(streamId, frameTypeId, payloadSize, false, buffer); } catch (ConnectionException e) { - // Will never happen because swallow() is called with mustBeZero set + // Will never happen because swallowPayload() is called with isPadding set // to false + } finally { + output.onSwallowedUnknownFrame(streamId, frameTypeId, flags, payloadSize); } - output.onSwallowedUnknownFrame(streamId, frameTypeId, flags, payloadSize); } @@ -550,33 +551,46 @@ class Http2Parser { log.debug(sm.getString("http2Parser.swallow.debug", connectionId, Integer.toString(streamId), Integer.toString(len))); } - if (len == 0) { - return; - } - if (!isPadding && byteBuffer != null) { - byteBuffer.position(byteBuffer.position() + len); - } else { - int read = 0; - byte[] buffer = new byte[1024]; - while (read < len) { - int thisTime = Math.min(buffer.length, len - read); - if (byteBuffer == null) { - input.fill(true, buffer, 0, thisTime); - } else { - byteBuffer.get(buffer, 0, thisTime); - } - if (isPadding) { - // Validate the padding is zero since receiving non-zero padding - // is a strong indication of either a faulty client or a server - // side bug. - for (int i = 0; i < thisTime; i++) { - if (buffer[i] != 0) { - throw new ConnectionException(sm.getString("http2Parser.nonZeroPadding", - connectionId, Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR); + try { + if (len == 0) { + return; + } + if (!isPadding && byteBuffer != null) { + byteBuffer.position(byteBuffer.position() + len); + } else { + int read = 0; + byte[] buffer = new byte[1024]; + while (read < len) { + int thisTime = Math.min(buffer.length, len - read); + if (byteBuffer == null) { + input.fill(true, buffer, 0, thisTime); + } else { + byteBuffer.get(buffer, 0, thisTime); + } + if (isPadding) { + // Validate the padding is zero since receiving non-zero padding + // is a strong indication of either a faulty client or a server + // side bug. + for (int i = 0; i < thisTime; i++) { + if (buffer[i] != 0) { + throw new ConnectionException(sm.getString("http2Parser.nonZeroPadding", + connectionId, Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR); + } } } + read += thisTime; + } + } + } finally { + if (FrameType.DATA.getIdByte() == frameTypeId) { + if (isPadding) { + // Need to add 1 for the padding length bytes that was also + // part of the payload. + len += 1; + } + if (len > 0) { + output.onSwallowedDataFramePayload(streamId, len); } - read += thisTime; } } } @@ -733,7 +747,7 @@ class Http2Parser { // Data frames ByteBuffer startRequestBodyFrame(int streamId, int payloadSize, boolean endOfStream) throws Http2Exception; - void endRequestBodyFrame(int streamId) throws Http2Exception; + void endRequestBodyFrame(int streamId) throws Http2Exception, IOException; void receivedEndOfStream(int streamId) throws ConnectionException; /** * Notification triggered when the parser swallows some or all of a DATA diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java b/java/org/apache/coyote/http2/Http2UpgradeHandler.java index ca6b7a7..9ce6b1d 100644 --- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java +++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java @@ -1467,20 +1467,14 @@ class Http2UpgradeHandler extends AbstractStream implements InternalHttpUpgradeH } AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId, true); - if (abstractNonZeroStream instanceof Stream) { - Stream stream = (Stream) abstractNonZeroStream; - stream.checkState(FrameType.DATA); - stream.receivedData(payloadSize); - return stream.getInputByteBuffer(); - } else { - abstractNonZeroStream.checkState(FrameType.DATA); - return null; - } + abstractNonZeroStream.checkState(FrameType.DATA); + abstractNonZeroStream.receivedData(payloadSize); + return abstractNonZeroStream.getInputByteBuffer(); } @Override - public void endRequestBodyFrame(int streamId) throws Http2Exception { + public void endRequestBodyFrame(int streamId) throws Http2Exception, IOException { AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId, true); if (abstractNonZeroStream instanceof Stream) { ((Stream) abstractNonZeroStream).getInputBuffer().onDataAvailable(); @@ -1503,11 +1497,9 @@ class Http2UpgradeHandler extends AbstractStream implements InternalHttpUpgradeH @Override - public void onSwallowedDataFramePayload(int streamId, int swallowedDataBytesCount) throws - ConnectionException, IOException { - AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId, true); - // +1 is for the payload byte used to define the padding length - writeWindowUpdate(abstractNonZeroStream, swallowedDataBytesCount + 1, false); + public void onSwallowedDataFramePayload(int streamId, int swallowedDataBytesCount) throws IOException { + AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId); + writeWindowUpdate(abstractNonZeroStream, swallowedDataBytesCount, false); } diff --git a/java/org/apache/coyote/http2/RecycledStream.java b/java/org/apache/coyote/http2/RecycledStream.java index b2b3e32..730936b 100644 --- a/java/org/apache/coyote/http2/RecycledStream.java +++ b/java/org/apache/coyote/http2/RecycledStream.java @@ -16,6 +16,8 @@ */ package org.apache.coyote.http2; +import java.nio.ByteBuffer; + /** * Represents a closed stream in the priority tree. Used in preference to the * full {@link Stream} as has much lower memory usage. @@ -23,10 +25,12 @@ package org.apache.coyote.http2; class RecycledStream extends AbstractNonZeroStream { private final String connectionId; + private int remainingFlowControlWindow; - RecycledStream(String connectionId, Integer identifier, StreamStateMachine state) { + RecycledStream(String connectionId, Integer identifier, StreamStateMachine state, int remainingFlowControlWindow) { super(identifier, state); this.connectionId = connectionId; + this.remainingFlowControlWindow = remainingFlowControlWindow; } @@ -41,5 +45,27 @@ class RecycledStream extends AbstractNonZeroStream { void incrementWindowSize(int increment) throws Http2Exception { // NO-OP } -} + + @Override + void receivedData(int payloadSize) throws ConnectionException { + remainingFlowControlWindow -= payloadSize; + } + + + /** + * {@inheritDoc} + * <p> + * This implementation will return an zero length ByteBuffer to trigger a + * flow control error if more DATA frame payload than the remaining flow + * control window is received for this recycled stream. + */ + @Override + ByteBuffer getInputByteBuffer() { + if (remainingFlowControlWindow < 0) { + return ZERO_LENGTH_BYTEBUFFER; + } else { + return null; + } + } +} diff --git a/java/org/apache/coyote/http2/Stream.java b/java/org/apache/coyote/http2/Stream.java index 7a03f05..9fb2055 100644 --- a/java/org/apache/coyote/http2/Stream.java +++ b/java/org/apache/coyote/http2/Stream.java @@ -517,9 +517,13 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { } + @Override final ByteBuffer getInputByteBuffer() { if (inputBuffer == null) { - return null; + // This must either be a push or an HTTP upgrade. Either way there + // should not be a request body so return a zero length ByteBuffer + // to trigger a flow control error. + return ZERO_LENGTH_BYTEBUFFER; } return inputBuffer.getInBuffer(); } @@ -549,7 +553,8 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { } - final void receivedData(int payloadSize) throws ConnectionException { + @Override + final void receivedData(int payloadSize) throws Http2Exception { contentLengthReceived += payloadSize; long contentLengthHeader = coyoteRequest.getContentLengthLong(); if (contentLengthHeader > -1 && contentLengthReceived > contentLengthHeader) { @@ -650,6 +655,7 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { state.sendReset(); cancelAllocationRequests(); handler.sendStreamReset(se); + inputBuffer.swallowUnread(); } catch (IOException ioe) { ConnectionException ce = new ConnectionException( sm.getString("stream.reset.fail"), Http2Error.PROTOCOL_ERROR, ioe); @@ -674,7 +680,8 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { if (log.isDebugEnabled()) { log.debug(sm.getString("stream.recycle", getConnectionId(), getIdAsString())); } - handler.replaceStream(this, new RecycledStream(getConnectionId(), getIdentifier(), state)); + handler.replaceStream( + this, new RecycledStream(getConnectionId(), getIdentifier(), state, getInputByteBuffer().remaining())); } @@ -1003,7 +1010,8 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { // 'write mode'. private volatile ByteBuffer inBuffer; private volatile boolean readInterest; - private boolean resetReceived = false; + private volatile boolean closed; + private boolean resetReceived; @Override public final int doRead(ApplicationBufferHandler applicationBufferHandler) @@ -1113,8 +1121,10 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { /* * Called after placing some data in the inBuffer. */ - final synchronized boolean onDataAvailable() { - if (readInterest) { + final synchronized void onDataAvailable() throws IOException { + if (closed) { + swallowUnread(); + } else if (readInterest) { if (log.isDebugEnabled()) { log.debug(sm.getString("stream.inputBuffer.dispatch")); } @@ -1124,7 +1134,6 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { // the incoming connection and streams are processed on their // own. coyoteRequest.action(ActionCode.DISPATCH_EXECUTE, null); - return true; } else { if (log.isDebugEnabled()) { log.debug(sm.getString("stream.inputBuffer.signal")); @@ -1132,7 +1141,6 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { synchronized (inBuffer) { inBuffer.notifyAll(); } - return false; } } @@ -1149,13 +1157,13 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { private final void ensureBuffersExist() { - if (inBuffer == null) { + if (inBuffer == null && !closed) { // The client must obey Tomcat's window size when sending so // this is the initial window size set by Tomcat that the client // uses (i.e. the local setting is required here). int size = handler.getLocalSettings().getInitialWindowSize(); synchronized (this) { - if (inBuffer == null) { + if (inBuffer == null && !closed) { inBuffer = ByteBuffer.allocate(size); outBuffer = new byte[size]; } @@ -1180,5 +1188,19 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { } } } + + private final void swallowUnread() throws IOException { + if (inBuffer != null) { + synchronized (inputBuffer) { + closed = true; + int unreadByteCount = inBuffer.position(); + if (unreadByteCount > 0) { + inBuffer.position(0); + inBuffer.limit(inBuffer.limit() - unreadByteCount); + handler.onSwallowedDataFramePayload(getIdAsInt(), unreadByteCount); + } + } + } + } } } diff --git a/test/org/apache/coyote/http2/Http2TestBase.java b/test/org/apache/coyote/http2/Http2TestBase.java index ef1ccbe..5a00a91 100644 --- a/test/org/apache/coyote/http2/Http2TestBase.java +++ b/test/org/apache/coyote/http2/Http2TestBase.java @@ -1183,10 +1183,9 @@ public abstract class Http2TestBase extends TomcatBaseTest { @Override public void onSwallowedDataFramePayload(int streamId, int swallowedDataBytesCount) { - trace.append(streamId); - trace.append("-SwallowedDataFramePayload-["); - trace.append(swallowedDataBytesCount); - trace.append("]\n"); + // NO-OP + // Many tests swallow request bodies which triggers this + // notification. It is added to the trace to reduce noise. } diff --git a/test/org/apache/coyote/http2/TestFlowControl.java b/test/org/apache/coyote/http2/TestFlowControl.java new file mode 100644 index 0000000..3841a1d --- /dev/null +++ b/test/org/apache/coyote/http2/TestFlowControl.java @@ -0,0 +1,131 @@ +/* + * 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.io.IOException; +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.MimeHeaders; + +public class TestFlowControl extends Http2TestBase { + + /* + * https://tomcat.markmail.org/thread/lijsebphms7hr3zj + */ + @Test + public void testNotFound() throws Exception { + + http2Connect(); + + // Connection and per-stream flow control default to 64k-1 bytes + + // Set up a POST request to a non-existent end-point + // Generate headers + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + MimeHeaders headers = new MimeHeaders(); + headers.addValue(":method").setString("POST"); + headers.addValue(":scheme").setString("http"); + headers.addValue(":path").setString("/path-does-not-exist"); + headers.addValue(":authority").setString("localhost:" + getPort()); + headers.addValue("content-length").setLong(65536); + hpackEncoder.encode(headers, headersPayload); + + headersPayload.flip(); + + ByteUtil.setThreeBytes(headersFrameHeader, 0, headersPayload.limit()); + headersFrameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. end of headers (0x04) + headersFrameHeader[4] = 0x04; + // Stream id + ByteUtil.set31Bits(headersFrameHeader, 5, 3); + + writeFrame(headersFrameHeader, headersPayload); + + // Generate body + // Max data payload is 16k + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(16 * 1024); + + while (dataPayload.hasRemaining()) { + dataPayload.put((byte) 'x'); + } + dataPayload.flip(); + + // Size + ByteUtil.setThreeBytes(dataFrameHeader, 0, dataPayload.limit()); + ByteUtil.set31Bits(dataFrameHeader, 5, 3); + + // Read the 404 error page + // headers + parser.readFrame(true); + // body + parser.readFrame(true); + // reset (becasue the request body was not fully read) + parser.readFrame(true); + // Validate response + Assert.assertEquals( + "3-HeadersStart\n" + + "3-Header-[:status]-[404]\n" + + "3-Header-[content-type]-[text/html;charset=utf-8]\n" + + "3-Header-[content-language]-[en]\n" + + "3-Header-[content-length]-[692]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + + "3-HeadersEnd\n" + + "3-Body-692\n" + + "3-EndOfStream\n" + + "3-RST-[8]\n", output.getTrace()); + output.clearTrace(); + + // Write 3*16k=48k of request body + int count = 0; + while (count < 3) { + writeFrame(dataFrameHeader, dataPayload); + waitForWindowSize(0); + dataPayload.position(0); + count++; + } + + // EOS + dataFrameHeader[4] = 0x01; + writeFrame(dataFrameHeader, dataPayload); + waitForWindowSize(0); + } + + + /* + * This might be unnecessary but given the potential for timing differences + * across different systems a more robust approach seems prudent. + */ + private void waitForWindowSize(int streamId) throws Http2Exception, IOException { + String prefix = streamId + "-WindowSize-"; + boolean found = false; + String trace; + do { + parser.readFrame(true); + trace = output.getTrace(); + output.clearTrace(); + System.out.println(trace); + found = trace.startsWith(prefix); + } while (!found); + } +} + diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 27cf82b..7557d37 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -119,6 +119,12 @@ Simplify the closing on an HTTP/2 stream when an error condition is present. (markt) </scode> + <fix> + <bug>65179</bug>: Ensure that the connection level flow control window + from the client to the server is updated when handling DATA frames + received for completed streams else the flow control window may become + exhausted. (markt) + </fix> </changelog> </subsection> </section> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org