Skip to content

Commit e2aef20

Browse files
authored
Release file handles back to caller on failure to take ownership (#2715)
Motivation: When taking ownership of input and output file descriptors in a bootstrap the ownership of the file handles remains with the caller in the function taking ownership fails. This is currently where _takingOwnershipOfDescriptors uses NIOFileHandle to take ownership of descriptors but then experiences a later failure. Modifications: If an exception is throw in _takingOwnershipOfDescriptors release file descriptors from NIOFileHandle. Result: No crash on failure to close file handles before end of scoped usage.
1 parent d8bf55d commit e2aef20

File tree

2 files changed

+64
-5
lines changed

2 files changed

+64
-5
lines changed

Sources/NIOPosix/Bootstrap.swift

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,7 @@ public final class NIOPipeBootstrap {
19261926
private var channelInitializer: Optional<ChannelInitializerCallback>
19271927
@usableFromInline
19281928
internal var _channelOptions: ChannelOptions.Storage
1929+
private let hooks: any NIOPipeBootstrapHooks
19291930

19301931
/// Create a `NIOPipeBootstrap` on the `EventLoopGroup` `group`.
19311932
///
@@ -1956,6 +1957,19 @@ public final class NIOPipeBootstrap {
19561957
self._channelOptions = ChannelOptions.Storage()
19571958
self.group = group
19581959
self.channelInitializer = nil
1960+
self.hooks = DefaultNIOPipeBootstrapHooks()
1961+
}
1962+
1963+
/// Initialiser for hooked testing
1964+
init?(validatingGroup group: EventLoopGroup, hooks: any NIOPipeBootstrapHooks) {
1965+
guard NIOOnSocketsBootstraps.isCompatible(group: group) else {
1966+
return nil
1967+
}
1968+
1969+
self._channelOptions = ChannelOptions.Storage()
1970+
self.group = group
1971+
self.channelInitializer = nil
1972+
self.hooks = hooks
19591973
}
19601974

19611975
/// Initialize the connected `PipeChannel` with `initializer`. The most common task in initializer is to add
@@ -2281,11 +2295,18 @@ extension NIOPipeBootstrap {
22812295

22822296
inputFileHandle = input.flatMap { NIOFileHandle(descriptor: $0) }
22832297
outputFileHandle = output.flatMap { NIOFileHandle(descriptor: $0) }
2284-
channel = try PipeChannel(
2285-
eventLoop: eventLoop as! SelectableEventLoop,
2286-
inputPipe: inputFileHandle,
2287-
outputPipe: outputFileHandle
2288-
)
2298+
do {
2299+
channel = try self.hooks.makePipeChannel(
2300+
eventLoop: eventLoop as! SelectableEventLoop,
2301+
inputPipe: inputFileHandle,
2302+
outputPipe: outputFileHandle
2303+
)
2304+
} catch {
2305+
// Release file handles back to the caller in case of failure.
2306+
_ = try? inputFileHandle?.takeDescriptorOwnership()
2307+
_ = try? outputFileHandle?.takeDescriptorOwnership()
2308+
throw error
2309+
}
22892310
} catch {
22902311
return eventLoop.makeFailedFuture(error)
22912312
}
@@ -2327,3 +2348,17 @@ extension NIOPipeBootstrap {
23272348

23282349
@available(*, unavailable)
23292350
extension NIOPipeBootstrap: Sendable {}
2351+
2352+
protocol NIOPipeBootstrapHooks {
2353+
func makePipeChannel(eventLoop: SelectableEventLoop,
2354+
inputPipe: NIOFileHandle?,
2355+
outputPipe: NIOFileHandle?) throws -> PipeChannel
2356+
}
2357+
2358+
fileprivate struct DefaultNIOPipeBootstrapHooks: NIOPipeBootstrapHooks {
2359+
func makePipeChannel(eventLoop: SelectableEventLoop,
2360+
inputPipe: NIOFileHandle?,
2361+
outputPipe: NIOFileHandle?) throws -> PipeChannel {
2362+
return try PipeChannel(eventLoop: eventLoop, inputPipe: inputPipe, outputPipe: outputPipe)
2363+
}
2364+
}

Tests/NIOPosixTests/BootstrapTest.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,30 @@ class BootstrapTest: XCTestCase {
680680
.wait())
681681
}
682682
}
683+
684+
// There was a bug where file handle ownership was not released when creating pipe channels failed.
685+
func testReleaseFileHandleOnOwningFailure() {
686+
struct NIOPipeBootstrapHooksChannelFail: NIOPipeBootstrapHooks {
687+
func makePipeChannel(eventLoop: NIOPosix.SelectableEventLoop, inputPipe: NIOCore.NIOFileHandle?, outputPipe: NIOCore.NIOFileHandle?) throws -> NIOPosix.PipeChannel {
688+
throw IOError(errnoCode: EBADF, reason: "testing")
689+
}
690+
}
691+
692+
let sock = socket(NIOBSDSocket.ProtocolFamily.local.rawValue, NIOBSDSocket.SocketType.stream.rawValue, 0)
693+
defer {
694+
close(sock)
695+
}
696+
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
697+
defer {
698+
try! elg.syncShutdownGracefully()
699+
}
700+
701+
let bootstrap = NIOPipeBootstrap(validatingGroup: elg, hooks: NIOPipeBootstrapHooksChannelFail())
702+
XCTAssertNotNil(bootstrap)
703+
704+
let channelFuture = bootstrap?.takingOwnershipOfDescriptor(inputOutput: sock)
705+
XCTAssertThrowsError(try channelFuture?.wait())
706+
}
683707
}
684708

685709
private final class WriteStringOnChannelActive: ChannelInboundHandler {

0 commit comments

Comments
 (0)