Skip to content

Commit aee4aad

Browse files
weissinormanmaurer
authored andcommitted
fix a crash when async connect failed (#302) (#309)
Motivation: When an asynchronous connect failed we would crash because we didn't correctly close the connection to start tearing everything down. That correctly made the `SocketChannelLifecycleManager` trip. This was filed as #302. Modifications: Correctly deregister and close the connection if an async connect fails. Result: Fewer crashes, fixes #302.
1 parent 241aed7 commit aee4aad

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

Sources/NIO/BaseSocketChannel.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,9 @@ class BaseSocketChannel<T: BaseSocket>: SelectableChannel, ChannelCore {
735735
do {
736736
try finishConnectSocket()
737737
} catch {
738-
connectPromise.fail(error: error)
738+
assert(!self.lifecycleManager.isActive)
739+
// close0 fails the connectPromise itself so no need to do it here
740+
self.close0(error: error, mode: .all, promise: nil)
739741
return
740742
}
741743
// We already know what the local address is.

Tests/NIOTests/ChannelTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ extension ChannelTests {
6363
("testChannelReadsDoesNotHappenAfterRegistration", testChannelReadsDoesNotHappenAfterRegistration),
6464
("testAppropriateAndInappropriateOperationsForUnregisteredSockets", testAppropriateAndInappropriateOperationsForUnregisteredSockets),
6565
("testCloseSocketWhenReadErrorWasReceivedAndMakeSureNoReadCompleteArrives", testCloseSocketWhenReadErrorWasReceivedAndMakeSureNoReadCompleteArrives),
66+
("testSocketFailingAsyncCorrectlyTearsTheChannelDownAndDoesntCrash", testSocketFailingAsyncCorrectlyTearsTheChannelDownAndDoesntCrash),
6667
]
6768
}
6869
}

Tests/NIOTests/ChannelTests.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,4 +2019,81 @@ public class ChannelTests: XCTestCase {
20192019
try allDone.futureResult.wait()
20202020
XCTAssertNoThrow(try sc.syncCloseAcceptingAlreadyClosed())
20212021
}
2022+
2023+
func testSocketFailingAsyncCorrectlyTearsTheChannelDownAndDoesntCrash() throws {
2024+
// regression test for #302
2025+
enum DummyError: Error { case dummy }
2026+
class SocketFailingAsyncConnect: Socket {
2027+
init() throws {
2028+
try super.init(protocolFamily: PF_INET, type: Posix.SOCK_STREAM, setNonBlocking: true)
2029+
}
2030+
2031+
override func connect(to address: SocketAddress) throws -> Bool {
2032+
_ = try super.connect(to: address)
2033+
return false
2034+
}
2035+
2036+
override func finishConnect() throws {
2037+
throw DummyError.dummy
2038+
}
2039+
}
2040+
2041+
class VerifyThingsAreRightHandler: ChannelInboundHandler {
2042+
typealias InboundIn = Never
2043+
private let allDone: EventLoopPromise<Void>
2044+
enum State {
2045+
case fresh
2046+
case registered
2047+
case unregistered
2048+
}
2049+
private var state: State = .fresh
2050+
2051+
init(allDone: EventLoopPromise<Void>) {
2052+
self.allDone = allDone
2053+
}
2054+
deinit { XCTAssertEqual(.unregistered, self.state) }
2055+
2056+
func channelActive(ctx: ChannelHandlerContext) { XCTFail("should never become active") }
2057+
2058+
func channelRead(ctx: ChannelHandlerContext, data: NIOAny) { XCTFail("should never read") }
2059+
2060+
func channelReadComplete(ctx: ChannelHandlerContext) { XCTFail("should never readComplete") }
2061+
2062+
func errorCaught(ctx: ChannelHandlerContext, error: Error) { XCTFail("pipeline shouldn't be told about connect error") }
2063+
2064+
func channelRegistered(ctx: ChannelHandlerContext) {
2065+
XCTAssertEqual(.fresh, self.state)
2066+
self.state = .registered
2067+
}
2068+
2069+
func channelUnregistered(ctx: ChannelHandlerContext) {
2070+
XCTAssertEqual(.registered, self.state)
2071+
self.state = .unregistered
2072+
self.allDone.succeed(result: ())
2073+
}
2074+
}
2075+
let group = MultiThreadedEventLoopGroup(numThreads: 2)
2076+
defer {
2077+
XCTAssertNoThrow(try group.syncShutdownGracefully())
2078+
}
2079+
let sc = try SocketChannel(socket: SocketFailingAsyncConnect(), eventLoop: group.next() as! SelectableEventLoop)
2080+
2081+
let serverChannel = try ServerBootstrap(group: group.next())
2082+
.bind(host: "127.0.0.1", port: 0)
2083+
.wait()
2084+
defer {
2085+
XCTAssertNoThrow(try serverChannel.syncCloseAcceptingAlreadyClosed())
2086+
}
2087+
2088+
let allDone: EventLoopPromise<Void> = group.next().newPromise()
2089+
XCTAssertNoThrow(try sc.eventLoop.submit {
2090+
sc.pipeline.add(handler: VerifyThingsAreRightHandler(allDone: allDone)).then {
2091+
sc.register().then {
2092+
sc.connect(to: serverChannel.localAddress!)
2093+
}
2094+
}
2095+
}.wait())
2096+
XCTAssertNoThrow(try allDone.futureResult.wait())
2097+
XCTAssertNoThrow(try sc.syncCloseAcceptingAlreadyClosed())
2098+
}
20222099
}

0 commit comments

Comments
 (0)