Skip to content

Commit e212169

Browse files
authored
Fix crash sending window increment after close (#95)
Motivation: When reads have yet to be delivered to a channel and we receive a CHANNEL_CLOSE message, we forcibly deliver all of the reads. As part of doing this, we process the window size changes and potentially send window increment messages. These messages are invalid to send at this time, and the state machine rightly forbids us from sending them, which causes a crash. Modifications: - Change the code to only futz with the channel window if the channel is still open. Result: No crashing after channel close.
1 parent c71c2ef commit e212169

File tree

3 files changed

+54
-9
lines changed

3 files changed

+54
-9
lines changed

Sources/NIOSSH/Child Channels/SSHChildChannel.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,8 @@ private extension SSHChildChannel {
640640
private func deliverSingleRead(_ data: PendingContent) {
641641
switch data {
642642
case .data(let data):
643-
if let increment = self.windowManager.unbufferBytes(data.data.readableBytes) {
643+
// We only futz with the window manager if the channel is not already closed.
644+
if !self.didClose, let increment = self.windowManager.unbufferBytes(data.data.readableBytes) {
644645
let update = SSHMessage.ChannelWindowAdjustMessage(recipientChannel: self.state.remoteChannelIdentifier!, bytesToAdd: UInt32(increment))
645646
self.processOutboundMessage(.channelWindowAdjust(update), promise: nil)
646647
}

Tests/NIOSSHTests/ChildChannelMultiplexerTests.swift

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the SwiftNIO open source project
44
//
5-
// Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
5+
// Copyright (c) 2019-2021 Apple Inc. and the SwiftNIO project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -1192,12 +1192,7 @@ final class ChildChannelMultiplexerTests: XCTestCase {
11921192
XCTAssertNoThrow(try harness.multiplexer.receiveMessage(self.openConfirmation(originalChannelID: channelID!, peerChannelID: 1)))
11931193

11941194
// The default window size is 1<<24 bytes. Sadly, we need a buffer that size.
1195-
// Sorry for the unsafe code, but otherwise this test takes _ages_.
1196-
var buffer = channel.allocator.buffer(capacity: (1 << 24) + 1)
1197-
buffer.writeWithUnsafeMutableBytes(minimumWritableBytes: (1 << 24) + 1) { ptr in
1198-
memset(ptr.baseAddress!, 0, (1 << 24) + 1)
1199-
return (1 << 24) + 1
1200-
}
1195+
let buffer = ByteBuffer.bigBuffer
12011196

12021197
// We're going to write one byte short.
12031198
XCTAssertEqual(harness.flushedMessages.count, 1)
@@ -1237,6 +1232,46 @@ final class ChildChannelMultiplexerTests: XCTestCase {
12371232
self.assertChannelClose(harness.flushedMessages.last, recipientChannel: 1)
12381233
}
12391234

1235+
func testWeDontResizeTheWindowOnClose() throws {
1236+
let harness = self.harnessForbiddingInboundChannels()
1237+
defer {
1238+
harness.finish()
1239+
}
1240+
1241+
var childChannel: Channel?
1242+
harness.multiplexer.createChildChannel(channelType: .session) { channel, _ in
1243+
childChannel = channel
1244+
return channel.setOption(ChannelOptions.autoRead, value: false)
1245+
}
1246+
1247+
guard let channel = childChannel else {
1248+
XCTFail("Did not create child channel")
1249+
return
1250+
}
1251+
1252+
// Activate channel.
1253+
let channelID = self.assertChannelOpen(harness.flushedMessages.first)
1254+
XCTAssertNoThrow(try harness.multiplexer.receiveMessage(self.openConfirmation(originalChannelID: channelID!, peerChannelID: 1)))
1255+
1256+
// The default window size is 1<<24 bytes. Sadly, we need a buffer that size.
1257+
let buffer = ByteBuffer.bigBuffer
1258+
1259+
// We're going to write the whole window.
1260+
XCTAssertEqual(harness.flushedMessages.count, 1)
1261+
XCTAssertNoThrow(try harness.multiplexer.receiveMessage(self.data(peerChannelID: channelID!,
1262+
data: buffer.getSlice(at: buffer.readerIndex, length: 1 << 24)!)))
1263+
1264+
// Auto read is off, so nothing happens.
1265+
XCTAssertEqual(harness.flushedMessages.count, 1)
1266+
1267+
// Now we send a close message. This is going to forcibly close the channel immediately.
1268+
XCTAssertNoThrow(try harness.multiplexer.receiveMessage(self.close(peerChannelID: channelID!)))
1269+
1270+
// This should trigger no message.
1271+
// (Actually, it should trigger a close, but that's a different bug and to be fixed in a different patch.)
1272+
XCTAssertEqual(harness.flushedMessages.count, 1)
1273+
}
1274+
12401275
func testRespectingMaxMessageSize() throws {
12411276
let harness = self.harnessForbiddingInboundChannels()
12421277
defer {
@@ -1694,3 +1729,12 @@ final class ChildChannelMultiplexerTests: XCTestCase {
16941729
self.assertChannelOpenConfirmation(harness.flushedMessages.first, recipientChannel: 1)
16951730
}
16961731
}
1732+
1733+
extension ByteBuffer {
1734+
/// A buffer `(1 << 24) + 1` bytes large.
1735+
fileprivate static let bigBuffer: ByteBuffer = {
1736+
// The default window size is 1<<24 bytes. Sadly, we need a buffer that size.
1737+
// We store it in a static so that we don't have to re-create it for every test.
1738+
ByteBuffer(repeating: 0, count: (1 << 24) + 1)
1739+
}()
1740+
}

scripts/soundness.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
1818

1919
function replace_acceptable_years() {
2020
# this needs to replace all acceptable forms with 'YEARS'
21-
sed -e 's/2017-20[12][890]/YEARS/' -e 's/2019-2020/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/'
21+
sed -e 's/20[12][78901]-20[12][8901]/YEARS/' -e 's/2019-2020/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/'
2222
}
2323

2424
command -v swiftformat >/dev/null 2>&1 || { echo >&2 "swiftformat needs to be installed but is not available in the path."; exit 1; }

0 commit comments

Comments
 (0)