Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions Sources/ConsoleKit/Terminal/readpassphrase_linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Darwin
#elseif canImport(Musl)
@preconcurrency import Musl
#endif
//import Dispatch

/// This implementation of `readpassphrase()`, used only on Linux where it's extremely difficult to get at the `libbsd`
/// API even when it is definitely present, is even less tolerant of being called on multiple threads at once than the
Expand Down Expand Up @@ -54,11 +53,6 @@ internal func linux_readpassphrase(
// Reset the signal counts and install a recovery handler onto a whole buncha signals
linux_readpassphrase_signos.reset()
var sigrecovery = sigaction()
var sigsave = sigaction()
var sigsaves: [Int32: sigaction] = [
SIGALRM: .init(), SIGHUP: .init(), SIGINT: .init(), SIGPIPE: .init(), SIGQUIT: .init(),
SIGTERM: .init(), SIGTSTP: .init(), SIGTTIN: .init(), SIGTTOU: .init(),
]
sigemptyset(&sigrecovery.sa_mask)
sigrecovery.sa_flags = 0
#if canImport(Darwin)
Expand All @@ -70,10 +64,7 @@ internal func linux_readpassphrase(
#elseif os(Android)
sigrecovery.sa_handler = { linux_readpassphrase_signos[$0] += 1 }
#endif
for (sig, _) in sigsaves {
sigaction(sig, &sigrecovery, &sigsave)
sigsaves[sig] = sigsave
}
let sigsaves = linux_readpassphrase_installHandlers(linux_readpassphrase_signals, &sigrecovery)

// Loop over a read() call, character by character. At the end, null-terminate. If echo is disabled, write a newline.
var i = 0
Expand Down Expand Up @@ -102,7 +93,7 @@ internal func linux_readpassphrase(
}

// Restore signal handlers
for (sig, var sa) in sigsaves { sigaction(sig, &sa, nil) }
linux_readpassphrase_restoreHandlers(linux_readpassphrase_signals, sigsaves)

// libbsd closes the TTY fd here. Since we deferred the fd closure, we just hope the difference doesn't cause problems.

Expand All @@ -120,6 +111,31 @@ internal func linux_readpassphrase(
return nr == -1 ? nil : buf
}

// MARK: - Workaround for https://github.com/swiftlang/swift/issues/91387
internal let linux_readpassphrase_signals: [Int32] = [
SIGALRM, SIGHUP, SIGINT, SIGPIPE, SIGQUIT, SIGTERM, SIGTSTP, SIGTTIN, SIGTTOU,
]

internal func linux_readpassphrase_installHandlers(_ signals: [Int32], _ handler: inout sigaction) -> [sigaction] {
var saved: [sigaction] = .init(repeating: .init(), count: signals.count)
var previous = sigaction()

for (i, signo) in signals.enumerated() {
sigaction(signo, &handler, &previous)
saved[i] = previous
}
return saved
}

internal func linux_readpassphrase_restoreHandlers(_ signals: [Int32], _ saved: [sigaction]) {
precondition(signals.count == saved.count, "Each signal must have exactly one saved disposition")

for (i, signo) in signals.enumerated() {
var sa = saved[i]
sigaction(signo, &sa, nil)
}
}

/// Used for signal recovery by `linux_readpassphrase()`. This is `static volatile` storage in the original.
/// We must avoid any accesses into the Swift runtime in the signal handler, so this is manually allocated
/// storage rather than a simple array. It is never deallocated and will be considered a leak by memory
Expand Down
79 changes: 79 additions & 0 deletions Tests/ConsoleKitTests/ReadPassphraseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#if (os(Linux) || os(Android)) || (os(macOS) && DEBUG)
@testable import ConsoleKit
import Testing

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Android)
import Android
#elseif canImport(Musl)
import Musl
#endif

// Regression tests for https://github.com/swiftlang/swift/issues/91387
@Suite("readpassphrase Tests", .serialized)
struct ReadPassphraseTests {
private func handler(of sa: sigaction) -> UInt {
#if canImport(Darwin)
return unsafeBitCast(sa.__sigaction_u.__sa_handler, to: UInt.self)
#elseif canImport(Glibc)
return unsafeBitCast(sa.__sigaction_handler.sa_handler, to: UInt.self)
#elseif canImport(Musl)
return unsafeBitCast(sa.__sa_handler.sa_handler, to: UInt.self)
#elseif os(Android)
return unsafeBitCast(sa.sa_handler, to: UInt.self)
#endif
}

private func currentHandler(_ signo: Int32) -> UInt {
var sa = sigaction()
sigaction(signo, nil, &sa)
return self.handler(of: sa)
}

private func makeRecoveryHandler() -> sigaction {
var sa = sigaction()
sigemptyset(&sa.sa_mask)
sa.sa_flags = 0
#if canImport(Darwin)
sa.__sigaction_u = .init(__sa_handler: { _ in })
#elseif canImport(Glibc)
sa.__sigaction_handler = .init(sa_handler: { _ in })
#elseif canImport(Musl)
sa.__sa_handler = .init(sa_handler: { _ in })
#elseif os(Android)
sa.sa_handler = { _ in }
#endif
return sa
}

@Test("Signal list has no duplicates")
func signalListIsUnique() {
#expect(linux_readpassphrase_signals.count == Set(linux_readpassphrase_signals).count)
}

@Test("Signal dispositions are saved and restored", .bug("https://github.com/vapor/console-kit/issues/235"))
func signalDispositionsRoundTrip() {
let signals = linux_readpassphrase_signals
let original = signals.map(self.currentHandler)

var recovery = self.makeRecoveryHandler()
let saved = linux_readpassphrase_installHandlers(signals, &recovery)

#expect(saved.count == signals.count)

let recoveryHandler = self.handler(of: recovery)
for signo in signals {
#expect(self.currentHandler(signo) == recoveryHandler, "signal \(signo) did not get the recovery handler")
}

linux_readpassphrase_restoreHandlers(signals, saved)

for (signo, before) in zip(signals, original) {
#expect(self.currentHandler(signo) == before, "signal \(signo) was not restored")
}
}
}
#endif
Loading