Skip to content

Commit cbc422e

Browse files
committed
Mitigate zero-length writes issue.
Motivation: On some iOS releases, zero-length writes cause problems for Network.framework. These problems can lead to difficult to debug catastrophic failure modes, so they ought to be avoided. NIOTransportServices provides a workaround channel handler, which we ought to use. Modifications: - Add a PlatformSupport check for whether to apply the workaround. - Add the workaround code to the pipeline configuration callbacks. - Tests Results: Users can work around the zero length writes problem.
1 parent fa00728 commit cbc422e

File tree

7 files changed

+312
-3
lines changed

7 files changed

+312
-3
lines changed

Sources/GRPC/ClientConnection.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
import Foundation
1717
import NIO
18+
import NIOTransportServices
1819
import NIOHTTP2
1920
import NIOSSL
2021
import NIOTLS
@@ -633,13 +634,14 @@ extension Channel {
633634
connectionKeepalive: ClientConnectionKeepalive,
634635
connectionIdleTimeout: TimeAmount,
635636
errorDelegate: ClientErrorDelegate?,
637+
requiresZeroLengthWriteWorkaround: Bool,
636638
logger: Logger
637639
) -> EventLoopFuture<Void> {
638640
let tlsConfigured = tlsConfiguration.map {
639641
self.configureTLS($0, serverHostname: tlsServerHostname, errorDelegate: errorDelegate, logger: logger)
640642
}
641643

642-
return (tlsConfigured ?? self.eventLoop.makeSucceededFuture(())).flatMap {
644+
let configuration: EventLoopFuture<Void> = (tlsConfigured ?? self.eventLoop.makeSucceededFuture(())).flatMap {
643645
self.configureHTTP2Pipeline(mode: .client, targetWindowSize: httpTargetWindowSize)
644646
}.flatMap { _ in
645647
return self.pipeline.handler(type: NIOHTTP2Handler.self).flatMap { http2Handler in
@@ -656,6 +658,19 @@ extension Channel {
656658
return self.pipeline.addHandler(errorHandler)
657659
}
658660
}
661+
662+
#if canImport(Network)
663+
// This availability guard is arguably unnecessary, but we add it anyway.
664+
if requiresZeroLengthWriteWorkaround, #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
665+
return configuration.flatMap {
666+
self.pipeline.addHandler(NIOFilterEmptyWritesHandler(), position: .first)
667+
}
668+
} else {
669+
return configuration
670+
}
671+
#else
672+
return configuration
673+
#endif
659674
}
660675

661676
func configureGRPCClient(

Sources/GRPC/ConnectionManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ extension ConnectionManager {
611611
connectionKeepalive: configuration.connectionKeepalive,
612612
connectionIdleTimeout: configuration.connectionIdleTimeout,
613613
errorDelegate: configuration.errorDelegate,
614+
requiresZeroLengthWriteWorkaround: PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.eventLoop, hasTLS: configuration.tls != nil),
614615
logger: self.logger
615616
)
616617

Sources/GRPC/PlatformSupport.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,24 @@ public enum PlatformSupport {
227227
logger.debug("creating a ServerBootstrap")
228228
return ServerBootstrap(group: group)
229229
}
230+
231+
/// Determines whether we may need to work around an issue in Network.framework with zero-length writes.
232+
///
233+
/// See https://github.com/apple/swift-nio-transport-services/pull/72 for more.
234+
static func requiresZeroLengthWriteWorkaround(group: EventLoopGroup, hasTLS: Bool) -> Bool {
235+
#if canImport(Network)
236+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
237+
if group is NIOTSEventLoopGroup || group is QoSEventLoop {
238+
// We need the zero-length write workaround on NIOTS when not using TLS.
239+
return !hasTLS
240+
} else {
241+
return false
242+
}
243+
} else {
244+
return false
245+
}
246+
#else
247+
return false
248+
#endif
249+
}
230250
}

Sources/GRPC/Server.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
import Foundation
1717
import NIO
18+
import NIOTransportServices
1819
import NIOHTTP1
1920
import NIOHTTP2
2021
import NIOSSL
@@ -115,7 +116,7 @@ public final class Server {
115116
return channel.pipeline.addHandler(handler)
116117
}
117118

118-
let configured: EventLoopFuture<Void>
119+
var configured: EventLoopFuture<Void>
119120

120121
if let tls = configuration.tls {
121122
configured = channel.configureTLS(configuration: tls).flatMap {
@@ -125,6 +126,14 @@ public final class Server {
125126
configured = channel.pipeline.addHandler(protocolSwitcher)
126127
}
127128

129+
// Work around the zero length write issue, if needed.
130+
let requiresZeroLengthWorkaround = PlatformSupport.requiresZeroLengthWriteWorkaround(group: configuration.eventLoopGroup, hasTLS: configuration.tls != nil)
131+
if requiresZeroLengthWorkaround, #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
132+
configured = configured.flatMap {
133+
channel.pipeline.addHandler(NIOFilterEmptyWritesHandler())
134+
}
135+
}
136+
128137
// Add the debug initializer, if there is one.
129138
if let debugAcceptedChannelInitializer = configuration.debugChannelInitializer {
130139
return configured.flatMap {

Tests/GRPCTests/PlatformSupportTests.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import Foundation
17-
import GRPC
17+
@testable import GRPC
1818
import NIO
1919
import NIOTransportServices
2020
import XCTest
@@ -128,4 +128,28 @@ class PlatformSupportTests: GRPCTestCase {
128128
XCTAssertTrue(bootstrap is NIOTSListenerBootstrap)
129129
#endif
130130
}
131+
132+
func testRequiresZeroLengthWorkaroundWithMTELG() {
133+
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
134+
135+
// No MTELG or individual loop requires the workaround.
136+
XCTAssertFalse(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: true))
137+
XCTAssertFalse(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: false))
138+
XCTAssertFalse(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: true))
139+
XCTAssertFalse(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: false))
140+
}
141+
142+
func testRequiresZeroLengthWorkaroundWithNetworkFramework() {
143+
// If we don't have Network.framework we can't test this.
144+
#if canImport(Network)
145+
guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return }
146+
self.group = NIOTSEventLoopGroup(loopCount: 1)
147+
148+
// We require the workaround for any of these loops when TLS is not enabled.
149+
XCTAssertFalse(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: true))
150+
XCTAssertTrue(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: false))
151+
XCTAssertFalse(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: true))
152+
XCTAssertTrue(PlatformSupport.requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: false))
153+
#endif
154+
}
131155
}

Tests/GRPCTests/XCTestManifests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,8 @@ extension PlatformSupportTests {
765765
("testMakeServerBootstrapReturnsNIOTSListenerBootstrapForQoSEventLoop", testMakeServerBootstrapReturnsNIOTSListenerBootstrapForQoSEventLoop),
766766
("testMakeServerBootstrapReturnsServerBootstrapForEventLoop", testMakeServerBootstrapReturnsServerBootstrapForEventLoop),
767767
("testMakeServerBootstrapReturnsServerBootstrapForMultiThreadedGroup", testMakeServerBootstrapReturnsServerBootstrapForMultiThreadedGroup),
768+
("testRequiresZeroLengthWorkaroundWithMTELG", testRequiresZeroLengthWorkaroundWithMTELG),
769+
("testRequiresZeroLengthWorkaroundWithNetworkFramework", testRequiresZeroLengthWorkaroundWithNetworkFramework),
768770
]
769771
}
770772

@@ -895,6 +897,18 @@ extension TimeLimitTests {
895897
]
896898
}
897899

900+
extension ZeroLengthWriteTests {
901+
// DO NOT MODIFY: This is autogenerated, use:
902+
// `swift test --generate-linuxmain`
903+
// to regenerate.
904+
static let __allTests__ZeroLengthWriteTests = [
905+
("testZeroLengthWriteTestNetworkFrameworkInsecure", testZeroLengthWriteTestNetworkFrameworkInsecure),
906+
("testZeroLengthWriteTestNetworkFrameworkSecure", testZeroLengthWriteTestNetworkFrameworkSecure),
907+
("testZeroLengthWriteTestPosixInsecure", testZeroLengthWriteTestPosixInsecure),
908+
("testZeroLengthWriteTestPosixSecure", testZeroLengthWriteTestPosixSecure),
909+
]
910+
}
911+
898912
extension ZlibTests {
899913
// DO NOT MODIFY: This is autogenerated, use:
900914
// `swift test --generate-linuxmain`
@@ -973,6 +987,7 @@ public func __allTests() -> [XCTestCaseEntry] {
973987
testCase(StopwatchTests.__allTests__StopwatchTests),
974988
testCase(StreamingRequestClientCallTests.__allTests__StreamingRequestClientCallTests),
975989
testCase(TimeLimitTests.__allTests__TimeLimitTests),
990+
testCase(ZeroLengthWriteTests.__allTests__ZeroLengthWriteTests),
976991
testCase(ZlibTests.__allTests__ZlibTests),
977992
]
978993
}

0 commit comments

Comments
 (0)