Skip to content

Commit 15304f3

Browse files
committed
Don’t crash SourceKit-LSP if swift-format crashes while we write the source file to stdin
When swift-format crashes before we send all data to its stdin, we get a SIGPIPE when trying to write more data to its studio on Linux. This, in turn, takes SourceKit-LSP down. Do the same trick that we do when launching `JSONRPCConnection` of disabling SIGPIPE globally if we can’t disable it for individual pipes. rdar://147665695
1 parent 374554b commit 15304f3

File tree

4 files changed

+83
-12
lines changed

4 files changed

+83
-12
lines changed

Sources/LanguageServerProtocolJSONRPC/DisableSigpipe.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,20 @@ private let globallyIgnoredSIGPIPE: Bool = {
2525
_ = signal(SIGPIPE, SIG_IGN)
2626
return true
2727
}()
28+
#endif
2829

29-
internal func globallyDisableSigpipe() {
30+
/// We receive a `SIGPIPE` if we write to a pipe that points to a crashed process. This in particular happens if the
31+
/// target of a `JSONRPCConnection` has crashed and we try to send it a message or if swift-format crashes and we try
32+
/// to send the source file to it.
33+
///
34+
/// On Darwin, `DispatchIO` and TSC `WritableByteStream` ignore `SIGPIPE` for the pipes handled by it, but that features
35+
/// is not available on Linux.
36+
/// Instead, globally ignore `SIGPIPE` on Linux to prevent us from crashing if the `JSONRPCConnection`'s target crashes.
37+
package func globallyDisableSigpipeIfNeeded() {
38+
#if os(Linux) || os(Android)
3039
let haveWeIgnoredSIGPIEThisIsHereToTriggerIgnoringIt = globallyIgnoredSIGPIPE
3140
guard haveWeIgnoredSIGPIEThisIsHereToTriggerIgnoringIt else {
3241
fatalError("globallyIgnoredSIGPIPE should always be true")
3342
}
43+
#endif
3444
}
35-
36-
#endif

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,7 @@ public final class JSONRPCConnection: Connection {
124124
self.inputMirrorFile = inputMirrorFile
125125
self.outputMirrorFile = outputMirrorFile
126126
self.receiveHandler = nil
127-
#if os(Linux) || os(Android)
128-
// We receive a `SIGPIPE` if we write to a pipe that points to a crashed process. This in particular happens if the
129-
// target of a `JSONRPCConnection` has crashed and we try to send it a message.
130-
// On Darwin, `DispatchIO` ignores `SIGPIPE` for the pipes handled by it, but that features is not available on Linux.
131-
// Instead, globally ignore `SIGPIPE` on Linux to prevent us from crashing if the `JSONRPCConnection`'s target crashes.
132-
globallyDisableSigpipe()
133-
#endif
127+
globallyDisableSigpipeIfNeeded()
134128
state = .created
135129
self.messageRegistry = messageRegistry
136130

Sources/SourceKitLSP/Swift/DocumentFormatting.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import Foundation
1414
package import LanguageServerProtocol
1515
import LanguageServerProtocolExtensions
16+
import LanguageServerProtocolJSONRPC
1617
import SKLogging
1718
import SKUtilities
1819
import SwiftExtensions
@@ -22,6 +23,7 @@ import TSCExtensions
2223

2324
import struct TSCBasic.AbsolutePath
2425
import class TSCBasic.Process
26+
import protocol TSCBasic.WritableByteStream
2527

2628
fileprivate extension String {
2729
init?(bytes: [UInt8], encoding: Encoding) {
@@ -189,12 +191,22 @@ extension SwiftLanguageService {
189191
"\(utf8Range.lowerBound):\(utf8UpperBound)",
190192
]
191193
}
194+
globallyDisableSigpipeIfNeeded()
192195
let process = TSCBasic.Process(arguments: args)
193-
let writeStream = try process.launch()
196+
let writeStream: any WritableByteStream
197+
do {
198+
writeStream = try process.launch()
199+
} catch {
200+
throw ResponseError.unknown("Launching swift-format failed: \(error)")
201+
}
194202

195203
// Send the file to format to swift-format's stdin. That way we don't have to write it to a file.
196204
writeStream.send(snapshot.text)
197-
try writeStream.close()
205+
do {
206+
try writeStream.close()
207+
} catch {
208+
throw ResponseError.unknown("Writing to swift-format stdin failed: \(error)")
209+
}
198210

199211
let result = try await withTimeout(.seconds(60)) {
200212
try await process.waitUntilExitStoppingProcessOnTaskCancellation()

Tests/SourceKitLSPTests/FormattingTests.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ import LanguageServerProtocol
1414
import SKLogging
1515
import SKTestSupport
1616
import SourceKitLSP
17+
import SwiftExtensions
18+
import ToolchainRegistry
1719
import XCTest
1820

21+
import class TSCBasic.Process
22+
1923
final class FormattingTests: XCTestCase {
2024
func testFormatting() async throws {
2125
try await SkipUnless.swiftFormatSupportsDashToIndicateReadingFromStdin()
@@ -306,4 +310,57 @@ final class FormattingTests: XCTestCase {
306310
]
307311
)
308312
}
313+
314+
func testSwiftFormatCrashing() async throws {
315+
try await withTestScratchDir { scratchDir in
316+
let toolchain = try await unwrap(ToolchainRegistry.forTesting.default)
317+
318+
let crashingSwiftFilePath = scratchDir.appendingPathComponent("crashing-executable.swift")
319+
let crashingExecutablePath = scratchDir.appendingPathComponent("crashing-executable")
320+
try await "fatalError()".writeWithRetry(to: crashingSwiftFilePath)
321+
var compilerArguments = try [
322+
crashingSwiftFilePath.filePath,
323+
"-o",
324+
crashingExecutablePath.filePath,
325+
]
326+
if let defaultSDKPath {
327+
compilerArguments += ["-sdk", defaultSDKPath]
328+
}
329+
try await Process.checkNonZeroExit(arguments: [XCTUnwrap(toolchain.swiftc?.filePath)] + compilerArguments)
330+
331+
let toolchainRegistry = ToolchainRegistry(toolchains: [
332+
Toolchain(
333+
identifier: "\(toolchain.identifier)-crashing-swift-format",
334+
displayName: "\(toolchain.identifier) with crashing swift-format",
335+
path: toolchain.path,
336+
clang: toolchain.clang,
337+
swift: toolchain.swift,
338+
swiftc: toolchain.swiftc,
339+
swiftFormat: crashingExecutablePath,
340+
clangd: toolchain.clangd,
341+
sourcekitd: toolchain.sourcekitd,
342+
sourceKitClientPlugin: toolchain.sourceKitClientPlugin,
343+
sourceKitServicePlugin: toolchain.sourceKitServicePlugin,
344+
libIndexStore: toolchain.libIndexStore
345+
)
346+
])
347+
let testClient = try await TestSourceKitLSPClient(toolchainRegistry: toolchainRegistry)
348+
let uri = DocumentURI(for: .swift)
349+
testClient.openDocument(
350+
// Generate a large source file to increase the chance of the executable we substitute for swift-format
351+
// crashing before the entire input file is sent to it.
352+
String(repeating: "func foo() {}\n", count: 10_000),
353+
uri: uri
354+
)
355+
await assertThrowsError(
356+
try await testClient.send(
357+
DocumentFormattingRequest(
358+
textDocument: TextDocumentIdentifier(uri),
359+
options: FormattingOptions(tabSize: 2, insertSpaces: true)
360+
)
361+
),
362+
expectedMessage: #/Running swift-format failed|Writing to swift-format stdin failed/#
363+
)
364+
}
365+
}
309366
}

0 commit comments

Comments
 (0)