Skip to content

Commit ff53cbc

Browse files
authored
Protect against HTT2ChannelHandler reentrancy (#319)
In rare cases the `HTTP2ChannelHandler`s method `unbufferAndFlushAutomaticFrames` and the `HTTP2StreaMultiplexer`s `newConnectionWindowSize` method were recursively calling themselves. This was causing a stack overflow. To protect our selves from this stack overflow I added a variable to protect ourselves against reentrancy inside the `unbufferAndFlushAutomaticFrames` method
1 parent 39ed0e7 commit ff53cbc

File tree

3 files changed

+65
-0
lines changed

3 files changed

+65
-0
lines changed

Sources/NIOHTTP2/HTTP2ChannelHandler.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
8181
/// to determine buffering strategies.
8282
private var channelWritable: Bool = true
8383

84+
/// This variable is set to `true` when we are inside `unbufferAndFlushAutomaticFrames` and protects us from reentering the method
85+
/// which can cause an infinite recursion.
86+
private var isUnbufferingAndFlushingAutomaticFrames = false
87+
8488
/// The mode for this parser to operate in: client or server.
8589
public enum ParserMode {
8690
/// Client mode
@@ -568,6 +572,13 @@ extension NIOHTTP2Handler {
568572
}
569573

570574
private func unbufferAndFlushAutomaticFrames(context: ChannelHandlerContext) {
575+
if self.isUnbufferingAndFlushingAutomaticFrames {
576+
// Don't allow infinite recursion through this method.
577+
return
578+
}
579+
580+
self.isUnbufferingAndFlushingAutomaticFrames = true
581+
571582
loop: while true {
572583
switch self.outboundBuffer.nextFlushedWritableFrame(channelWritable: self.channelWritable) {
573584
case .noFrame:
@@ -578,6 +589,8 @@ extension NIOHTTP2Handler {
578589
self.processOutboundFrame(context: context, frame: frame, promise: promise)
579590
}
580591
}
592+
593+
self.isUnbufferingAndFlushingAutomaticFrames = false
581594

582595
if self.wroteFrame {
583596
context.flush()

Tests/NIOHTTP2Tests/ReentrancyTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ extension ReentrancyTests {
3030
("testReEnterReadOnRead", testReEnterReadOnRead),
3131
("testReenterInactiveOnRead", testReenterInactiveOnRead),
3232
("testReenterReadEOFOnRead", testReenterReadEOFOnRead),
33+
("testReenterAutomaticFrames", testReenterAutomaticFrames),
3334
]
3435
}
3536
}

Tests/NIOHTTP2Tests/ReentrancyTests.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import NIOCore
1717
import NIOEmbedded
1818
import NIOHTTP1
1919
import NIOHTTP2
20+
import NIOHPACK
2021

2122
/// A `ChannelInboundHandler` that re-entrantly calls into a handler that just passed
2223
/// it `channelRead`.
@@ -43,6 +44,22 @@ final class ReenterOnReadHandler: ChannelInboundHandler {
4344
}
4445
}
4546

47+
final class WindowUpdatedEventHandler: ChannelInboundHandler {
48+
public typealias InboundIn = HTTP2Frame
49+
public typealias InboundOut = HTTP2Frame
50+
public typealias OutboundOut = HTTP2Frame
51+
52+
init() {
53+
}
54+
55+
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
56+
guard event is NIOHTTP2WindowUpdatedEvent else { return }
57+
58+
let frame = HTTP2Frame(streamID: .rootStream, payload: .windowUpdate(windowSizeIncrement: 1))
59+
context.writeAndFlush(self.wrapOutboundOut(frame), promise: nil)
60+
}
61+
}
62+
4663
// Tests that the HTTP2Parser is safely re-entrant.
4764
//
4865
// These tests ensure that we don't ever call into the HTTP/2 session more than once at a time.
@@ -164,4 +181,38 @@ final class ReentrancyTests: XCTestCase {
164181
XCTAssertNoThrow(try self.clientChannel.finish())
165182
XCTAssertNoThrow(try self.serverChannel.finish())
166183
}
184+
185+
func testReenterAutomaticFrames() throws {
186+
// Start by setting up the connection.
187+
try self.basicHTTP2Connection()
188+
let windowUpdateFrameHandler = WindowUpdatedEventHandler()
189+
XCTAssertNoThrow(try self.serverChannel.pipeline.addHandler(windowUpdateFrameHandler).wait())
190+
191+
// Write and flush the header from the client to open a stream
192+
let headers = HPACKHeaders([(":path", "/"), (":method", "POST"), (":scheme", "https"), (":authority", "localhost")])
193+
let reqFramePayload = HTTP2Frame.FramePayload.headers(.init(headers: headers))
194+
self.clientChannel.writeAndFlush(HTTP2Frame(streamID: 1, payload: reqFramePayload), promise: nil)
195+
self.interactInMemory(clientChannel, serverChannel)
196+
197+
// Write and flush the header from the server
198+
let resHeaders = HPACKHeaders([(":status", "200")])
199+
let resFramePayload = HTTP2Frame.FramePayload.headers(.init(headers: resHeaders))
200+
self.serverChannel.writeAndFlush(HTTP2Frame(streamID: 1, payload: resFramePayload), promise: nil)
201+
202+
// Write lots of small data frames and flush them all at once
203+
var requestBody = self.serverChannel.allocator.buffer(capacity: 1)
204+
requestBody.writeBytes(Array(repeating: UInt8(0x04), count: 1))
205+
var reqBodyFramePayload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(requestBody)))
206+
for _ in 0..<10000 {
207+
serverChannel.write(HTTP2Frame(streamID: 1, payload: reqBodyFramePayload), promise: nil)
208+
}
209+
reqBodyFramePayload = .data(.init(data: .byteBuffer(requestBody), endStream: true))
210+
serverChannel.writeAndFlush(HTTP2Frame(streamID: 1, payload: reqBodyFramePayload), promise: nil)
211+
212+
// Now we can deliver these bytes.
213+
self.deliverAllBytes(from: self.clientChannel, to: self.serverChannel)
214+
215+
XCTAssertNoThrow(try self.clientChannel.finish())
216+
XCTAssertNoThrow(try self.serverChannel.finish())
217+
}
167218
}

0 commit comments

Comments
 (0)