You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2021/09/13 16:05:09 UTC

[tomcat] 01/03: Avoid StackOverflowException

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

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

commit 3910ab722be9b791533f8662e45cc6b5701c3177
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Fri Sep 10 08:21:36 2021 +0100

    Avoid StackOverflowException
    
    On a fast network (e.g. localhost) the writes all complete in-line. That
    leads to the CompletionHandler triggering another write, which triggers
    the CompletionHandler which trigegrs another write and so on. With large
    response bodies and recursive in-line writes, it is easy to trigger a
    StackOverflowException.
---
 .../coyote/http2/Http2AsyncUpgradeHandler.java     | 127 ++++++++++++---------
 webapps/docs/changelog.xml                         |   4 +
 2 files changed, 75 insertions(+), 56 deletions(-)

diff --git a/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java b/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
index 4dc268a..19c88a1 100644
--- a/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
+++ b/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
@@ -36,6 +36,7 @@ import org.apache.tomcat.util.http.MimeHeaders;
 import org.apache.tomcat.util.net.SendfileState;
 import org.apache.tomcat.util.net.SocketWrapperBase;
 import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode;
+import org.apache.tomcat.util.net.SocketWrapperBase.CompletionState;
 
 public class Http2AsyncUpgradeHandler extends Http2UpgradeHandler {
 
@@ -363,70 +364,84 @@ public class Http2AsyncUpgradeHandler extends Http2UpgradeHandler {
     protected class SendfileCompletionHandler implements CompletionHandler<Long, SendfileData> {
         @Override
         public void completed(Long nBytes, SendfileData sendfile) {
+            CompletionState completionState = null;
             long bytesWritten = nBytes.longValue() - 9;
-            sendfile.left -= bytesWritten;
-            if (sendfile.left == 0) {
-                try {
-                    sendfile.stream.getOutputBuffer().end();
-                } catch (IOException e) {
-                    failed(e, sendfile);
+
+            /*
+             * Loop for in-line writes only. Avoids a possible stack-overflow of
+             * chained completion handlers with a long series of in-line writes.
+             */
+            do {
+                sendfile.left -= bytesWritten;
+                if (sendfile.left == 0) {
+                    try {
+                        sendfile.stream.getOutputBuffer().end();
+                    } catch (IOException e) {
+                        failed(e, sendfile);
+                    }
+                    return;
                 }
-                return;
-            }
-            sendfile.streamReservation -= bytesWritten;
-            sendfile.connectionReservation -= bytesWritten;
-            sendfile.pos += bytesWritten;
-            try {
-                if (sendfile.connectionReservation == 0) {
-                    if (sendfile.streamReservation == 0) {
-                        int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) (sendfile.end - sendfile.pos);
-                        sendfile.streamReservation = sendfile.stream.reserveWindowSize(reservation, true);
+                sendfile.streamReservation -= bytesWritten;
+                sendfile.connectionReservation -= bytesWritten;
+                sendfile.pos += bytesWritten;
+                try {
+                    if (sendfile.connectionReservation == 0) {
+                        if (sendfile.streamReservation == 0) {
+                            int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) (sendfile.end - sendfile.pos);
+                            sendfile.streamReservation = sendfile.stream.reserveWindowSize(reservation, true);
+                        }
+                        sendfile.connectionReservation = reserveWindowSize(sendfile.stream, sendfile.streamReservation, true);
                     }
-                    sendfile.connectionReservation = reserveWindowSize(sendfile.stream, sendfile.streamReservation, true);
+                } catch (IOException e) {
+                    failed (e, sendfile);
+                    return;
                 }
-            } catch (IOException e) {
-                failed (e, sendfile);
-                return;
-            }
 
-            if (log.isDebugEnabled()) {
-                log.debug(sm.getString("upgradeHandler.sendfile.reservation", connectionId, sendfile.stream.getIdAsString(),
-                        Integer.valueOf(sendfile.connectionReservation), Integer.valueOf(sendfile.streamReservation)));
-            }
-
-            // connectionReservation will always be smaller than or the same as
-            // streamReservation
-            int frameSize = Integer.min(getMaxFrameSize(), sendfile.connectionReservation);
-            boolean finished = (frameSize == sendfile.left) && sendfile.stream.getCoyoteResponse().getTrailerFields() == null;
-
-            // Need to check this now since sending end of stream will change this.
-            boolean writeable = sendfile.stream.canWrite();
-            byte[] header = new byte[9];
-            ByteUtil.setThreeBytes(header, 0, frameSize);
-            header[3] = FrameType.DATA.getIdByte();
-            if (finished) {
-                header[4] = FLAG_END_OF_STREAM;
-                sendfile.stream.sentEndOfStream();
-                if (!sendfile.stream.isActive()) {
-                    setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet());
-                }
-            }
-            if (writeable) {
                 if (log.isDebugEnabled()) {
-                    log.debug(sm.getString("upgradeHandler.writeBody", connectionId, sendfile.stream.getIdAsString(),
-                            Integer.toString(frameSize), Boolean.valueOf(finished)));
+                    log.debug(sm.getString("upgradeHandler.sendfile.reservation", connectionId, sendfile.stream.getIdAsString(),
+                            Integer.valueOf(sendfile.connectionReservation), Integer.valueOf(sendfile.streamReservation)));
                 }
-                ByteUtil.set31Bits(header, 5, sendfile.stream.getIdAsInt());
-                sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize);
-                socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(),
-                        TimeUnit.MILLISECONDS, sendfile, SocketWrapperBase.COMPLETE_WRITE_WITH_COMPLETION,
-                        this, ByteBuffer.wrap(header), sendfile.mappedBuffer);
-                try {
-                    handleAsyncException();
-                } catch (IOException e) {
-                    failed(e, sendfile);
+
+                // connectionReservation will always be smaller than or the same as
+                // streamReservation
+                int frameSize = Integer.min(getMaxFrameSize(), sendfile.connectionReservation);
+                boolean finished = (frameSize == sendfile.left) && sendfile.stream.getCoyoteResponse().getTrailerFields() == null;
+
+                // Need to check this now since sending end of stream will change this.
+                boolean writeable = sendfile.stream.canWrite();
+                byte[] header = new byte[9];
+                ByteUtil.setThreeBytes(header, 0, frameSize);
+                header[3] = FrameType.DATA.getIdByte();
+                if (finished) {
+                    header[4] = FLAG_END_OF_STREAM;
+                    sendfile.stream.sentEndOfStream();
+                    if (!sendfile.stream.isActive()) {
+                        setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet());
+                    }
                 }
-            }
+                if (writeable) {
+                    if (log.isDebugEnabled()) {
+                        log.debug(sm.getString("upgradeHandler.writeBody", connectionId, sendfile.stream.getIdAsString(),
+                                Integer.toString(frameSize), Boolean.valueOf(finished)));
+                    }
+                    ByteUtil.set31Bits(header, 5, sendfile.stream.getIdAsInt());
+                    sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize);
+                    // Note: Completion handler not called in the write
+                    //       completes in-line. The wrote will continue via the
+                    //       surrounding loop.
+                    completionState = socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(),
+                            TimeUnit.MILLISECONDS, sendfile, SocketWrapperBase.COMPLETE_WRITE,
+                            this, ByteBuffer.wrap(header), sendfile.mappedBuffer);
+                    try {
+                        handleAsyncException();
+                    } catch (IOException e) {
+                        failed(e, sendfile);
+                        return;
+                    }
+                }
+                // Update bytesWritten for start of next loop iteration
+                bytesWritten = frameSize;
+            } while (completionState == CompletionState.INLINE);
         }
 
         @Override
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 224fafd..8132f24 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -121,6 +121,10 @@
         after <code>bytes</code>. Fix based on pull request <pr>449</pr> by
         Thierry Guérin. (markt)
       </fix>
+      <fix>
+        Correct a potential <code>StackOverflowException</code> with HTTP/2 and
+        sendfile. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Jasper">

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org