Skip to content

Commit 254b42a

Browse files
authored
Merge pull request #2098 from ahoppen/swift-format-crashing
Don’t crash SourceKit-LSP if swift-format crashes while we write the source file to stdin
2 parents 24f361c + f3daf21 commit 254b42a

File tree

4 files changed

+112
-14
lines changed

4 files changed

+112
-14
lines changed

Sources/LanguageServerProtocolJSONRPC/DisableSigpipe.swift

+14-3
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,23 @@ 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` ignores `SIGPIPE` for the pipes handled by it and swift-tools-support-core offers
35+
/// `LocalFileOutputByteStream.disableSigpipe`, but that features is not available on Linux.
36+
///
37+
/// Instead, globally ignore `SIGPIPE` on Linux to prevent us from crashing if the `JSONRPCConnection`'s target crashes.
38+
///
39+
/// On Darwin platforms and on Windows this is a no-op.
40+
package func globallyDisableSigpipeIfNeeded() {
41+
#if !canImport(Darwin) && !os(Windows)
3042
let haveWeIgnoredSIGPIEThisIsHereToTriggerIgnoringIt = globallyIgnoredSIGPIPE
3143
guard haveWeIgnoredSIGPIEThisIsHereToTriggerIgnoringIt else {
3244
fatalError("globallyIgnoredSIGPIPE should always be true")
3345
}
46+
#endif
3447
}
35-
36-
#endif

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

+1-7
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

+40-4
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
@@ -21,7 +22,9 @@ import SwiftSyntax
2122
import TSCExtensions
2223

2324
import struct TSCBasic.AbsolutePath
25+
import class TSCBasic.LocalFileOutputByteStream
2426
import class TSCBasic.Process
27+
import protocol TSCBasic.WritableByteStream
2528

2629
fileprivate extension String {
2730
init?(bytes: [UInt8], encoding: Encoding) {
@@ -190,11 +193,44 @@ extension SwiftLanguageService {
190193
]
191194
}
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+
}
202+
#if canImport(Darwin)
203+
// On Darwin, we can disable SIGPIPE for a single pipe. This is not available on all platforms, in which case we
204+
// resort to disabling SIGPIPE globally to avoid crashing SourceKit-LSP with SIGPIPE if swift-format crashes before
205+
// we could send all data to its stdin.
206+
if let byteStream = writeStream as? LocalFileOutputByteStream {
207+
orLog("Disable SIGPIPE for swift-format stdin") {
208+
try byteStream.disableSigpipe()
209+
}
210+
} else {
211+
logger.fault("Expected write stream to process to be a LocalFileOutputByteStream")
212+
}
213+
#else
214+
globallyDisableSigpipeIfNeeded()
215+
#endif
194216

195-
// Send the file to format to swift-format's stdin. That way we don't have to write it to a file.
196-
writeStream.send(snapshot.text)
197-
try writeStream.close()
217+
do {
218+
// Send the file to format to swift-format's stdin. That way we don't have to write it to a file.
219+
//
220+
// If we are on Windows, `writeStream` is not a swift-tools-support-core type but a `FileHandle`. In that case,
221+
// call the throwing `write(contentsOf:)` method on it so that we can catch a `ERROR_BROKEN_PIPE` error. The
222+
// `send` method that we use on all other platforms ends up calling the non-throwing `FileHandle.write(_:)`, which
223+
// calls `write(contentsOf:)` using `try!` and thus crashes SourceKit-LSP if the pipe to swift-format is closed,
224+
// eg. because swift-format has crashed.
225+
if let fileHandle = writeStream as? FileHandle {
226+
try fileHandle.write(contentsOf: Data(snapshot.text.utf8))
227+
} else {
228+
writeStream.send(snapshot.text.utf8)
229+
}
230+
try writeStream.close()
231+
} catch {
232+
throw ResponseError.unknown("Writing to swift-format stdin failed: \(error)")
233+
}
198234

199235
let result = try await withTimeout(.seconds(60)) {
200236
try await process.waitUntilExitStoppingProcessOnTaskCancellation()

Tests/SourceKitLSPTests/FormattingTests.swift

+57
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)