From ea936eff20aa72ba4dc61c87e8afe8a2d7d37a83 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Tue, 11 Aug 2026 18:09:52 +0100 Subject: [PATCH 1/2] Add regression tests --- .../ConsoleKitTests/ReadPassphraseTests.swift | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Tests/ConsoleKitTests/ReadPassphraseTests.swift diff --git a/Tests/ConsoleKitTests/ReadPassphraseTests.swift b/Tests/ConsoleKitTests/ReadPassphraseTests.swift new file mode 100644 index 0000000..eab5909 --- /dev/null +++ b/Tests/ConsoleKitTests/ReadPassphraseTests.swift @@ -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 From 2c881ff24749569939184e001c3f4d8a1a29c129 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Tue, 11 Aug 2026 18:11:22 +0100 Subject: [PATCH 2/2] Workaround bug --- .../Terminal/readpassphrase_linux.swift | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift b/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift index 20e1676..89af717 100644 --- a/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift +++ b/Sources/ConsoleKit/Terminal/readpassphrase_linux.swift @@ -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 @@ -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) @@ -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 @@ -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. @@ -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