diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 805740b..d785884 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,6 +26,8 @@ jobs: windows_swift_versions: '["6.1", "nightly-main"]' enable_macos_checks: true macos_xcode_versions: '["16.3"]' + enable_linux_static_sdk_build: true + linux_static_sdk_versions: '["6.1", "nightly-6.2"]' soundness: name: Soundness diff --git a/Sources/Subprocess/IO/AsyncIO+Linux.swift b/Sources/Subprocess/IO/AsyncIO+Linux.swift index 783b04c..0ca318e 100644 --- a/Sources/Subprocess/IO/AsyncIO+Linux.swift +++ b/Sources/Subprocess/IO/AsyncIO+Linux.swift @@ -23,6 +23,7 @@ import Glibc #elseif canImport(Android) import Android +import posix_filesystem.sys_epoll #elseif canImport(Musl) import Musl #endif @@ -40,7 +41,7 @@ final class AsyncIO: Sendable { typealias OutputStream = AsyncThrowingStream - private final class MonitorThreadContext { + private struct MonitorThreadContext: Sendable { let epollFileDescriptor: CInt let shutdownFileDescriptor: CInt @@ -95,7 +96,7 @@ final class AsyncIO: Sendable { events: EPOLLIN.rawValue, data: epoll_data(fd: shutdownFileDescriptor) ) - var rc = epoll_ctl( + let rc = epoll_ctl( epollFileDescriptor, EPOLL_CTL_ADD, shutdownFileDescriptor, @@ -117,76 +118,71 @@ final class AsyncIO: Sendable { epollFileDescriptor: epollFileDescriptor, shutdownFileDescriptor: shutdownFileDescriptor ) - let threadContext = Unmanaged.passRetained(context) - var thread: pthread_t = pthread_t() - rc = pthread_create(&thread, nil, { args in - func reportError(_ error: SubprocessError) { - _registration.withLock { store in - for continuation in store.values { - continuation.finish(throwing: error) + let thread: pthread_t + do { + thread = try pthread_create { + func reportError(_ error: SubprocessError) { + _registration.withLock { store in + for continuation in store.values { + continuation.finish(throwing: error) + } } } - } - - let unmanaged = Unmanaged.fromOpaque(args!) - let context = unmanaged.takeRetainedValue() - - var events: [epoll_event] = Array( - repeating: epoll_event(events: 0, data: epoll_data(fd: 0)), - count: _epollEventSize - ) - // Enter the monitor loop - monitorLoop: while true { - let eventCount = epoll_wait( - context.epollFileDescriptor, - &events, - CInt(events.count), - -1 + var events: [epoll_event] = Array( + repeating: epoll_event(events: 0, data: epoll_data(fd: 0)), + count: _epollEventSize ) - if eventCount < 0 { - if errno == EINTR || errno == EAGAIN { - continue // interrupted by signal; try again - } - // Report other errors - let error = SubprocessError( - code: .init(.asyncIOFailed( - "epoll_wait failed") - ), - underlyingError: .init(rawValue: errno) - ) - reportError(error) - break monitorLoop - } - for index in 0 ..< Int(eventCount) { - let event = events[index] - let targetFileDescriptor = event.data.fd - // Breakout the monitor loop if we received shutdown - // from the shutdownFD - if targetFileDescriptor == context.shutdownFileDescriptor { - var buf: UInt64 = 0 - _ = _SubprocessCShims.read(context.shutdownFileDescriptor, &buf, MemoryLayout.size) + // Enter the monitor loop + monitorLoop: while true { + let eventCount = epoll_wait( + context.epollFileDescriptor, + &events, + CInt(events.count), + -1 + ) + if eventCount < 0 { + if errno == EINTR || errno == EAGAIN { + continue // interrupted by signal; try again + } + // Report other errors + let error = SubprocessError( + code: .init(.asyncIOFailed( + "epoll_wait failed") + ), + underlyingError: .init(rawValue: errno) + ) + reportError(error) break monitorLoop } - // Notify the continuation - let continuation = _registration.withLock { store -> SignalStream.Continuation? in - if let continuation = store[targetFileDescriptor] { - return continuation + for index in 0 ..< Int(eventCount) { + let event = events[index] + let targetFileDescriptor = event.data.fd + // Breakout the monitor loop if we received shutdown + // from the shutdownFD + if targetFileDescriptor == context.shutdownFileDescriptor { + var buf: UInt64 = 0 + _ = _subprocess_read(context.shutdownFileDescriptor, &buf, MemoryLayout.size) + break monitorLoop } - return nil + + // Notify the continuation + let continuation = _registration.withLock { store -> SignalStream.Continuation? in + if let continuation = store[targetFileDescriptor] { + return continuation + } + return nil + } + continuation?.yield(true) } - continuation?.yield(true) } } - - return nil - }, threadContext.toOpaque()) - guard rc == 0 else { + } catch let underlyingError { let error = SubprocessError( code: .init(.asyncIOFailed("Failed to create monitor thread")), - underlyingError: .init(rawValue: rc) + underlyingError: underlyingError ) self.state = .failure(error) return @@ -211,14 +207,14 @@ final class AsyncIO: Sendable { var one: UInt64 = 1 // Wake up the thread for shutdown - _ = _SubprocessCShims.write(currentState.shutdownFileDescriptor, &one, MemoryLayout.stride) + _ = _subprocess_write(currentState.shutdownFileDescriptor, &one, MemoryLayout.stride) // Cleanup the monitor thread pthread_join(currentState.monitorThread, nil) var closeError: CInt = 0 - if _SubprocessCShims.close(currentState.epollFileDescriptor) != 0 { + if _subprocess_close(currentState.epollFileDescriptor) != 0 { closeError = errno } - if _SubprocessCShims.close(currentState.shutdownFileDescriptor) != 0 { + if _subprocess_close(currentState.shutdownFileDescriptor) != 0 { closeError = errno } if closeError != 0 { @@ -231,7 +227,7 @@ final class AsyncIO: Sendable { _ fileDescriptor: FileDescriptor, for event: Event ) -> SignalStream { - return SignalStream { continuation in + return SignalStream { (continuation: SignalStream.Continuation) -> () in // If setup failed, nothing much we can do switch self.state { case .success(let state): @@ -261,9 +257,9 @@ final class AsyncIO: Sendable { let targetEvent: EPOLL_EVENTS switch event { case .read: - targetEvent = EPOLLIN + targetEvent = EPOLL_EVENTS(EPOLLIN) case .write: - targetEvent = EPOLLOUT + targetEvent = EPOLL_EVENTS(EPOLLOUT) } var event = epoll_event( @@ -369,7 +365,7 @@ extension AsyncIO { let offsetAddress = bufferPointer.baseAddress!.advanced(by: readLength) // Read directly into the buffer at the offset - return _SubprocessCShims.read(fileDescriptor.rawValue, offsetAddress, targetCount) + return _subprocess_read(fileDescriptor.rawValue, offsetAddress, targetCount) } if bytesRead > 0 { // Read some data @@ -435,7 +431,7 @@ extension AsyncIO { let written = bytes.withUnsafeBytes { ptr in let remainingLength = ptr.count - writtenLength let startPtr = ptr.baseAddress!.advanced(by: writtenLength) - return _SubprocessCShims.write(fileDescriptor.rawValue, startPtr, remainingLength) + return _subprocess_write(fileDescriptor.rawValue, startPtr, remainingLength) } if written > 0 { writtenLength += written @@ -478,7 +474,7 @@ extension AsyncIO { let written = span.withUnsafeBytes { ptr in let remainingLength = ptr.count - writtenLength let startPtr = ptr.baseAddress!.advanced(by: writtenLength) - return _SubprocessCShims.write(fileDescriptor.rawValue, startPtr, remainingLength) + return _subprocess_write(fileDescriptor.rawValue, startPtr, remainingLength) } if written > 0 { writtenLength += written diff --git a/Sources/Subprocess/Platforms/Subprocess+Linux.swift b/Sources/Subprocess/Platforms/Subprocess+Linux.swift index 4b18dd4..62eb232 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Linux.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Linux.swift @@ -19,10 +19,19 @@ #if canImport(Glibc) import Glibc +let _subprocess_read = Glibc.read +let _subprocess_write = Glibc.write +let _subprocess_close = Glibc.close #elseif canImport(Android) import Android +let _subprocess_read = Android.read +let _subprocess_write = Android.write +let _subprocess_close = Android.close #elseif canImport(Musl) import Musl +let _subprocess_read = Musl.read +let _subprocess_write = Musl.write +let _subprocess_close = Musl.close #endif internal import Dispatch @@ -31,6 +40,42 @@ import Synchronization import _SubprocessCShims // Linux specific implementations +#if canImport(Glibc) +extension EPOLL_EVENTS { + init(_ other: EPOLL_EVENTS) { + self = other + } +} +#elseif canImport(Bionic) +typealias EPOLL_EVENTS = CInt + +extension UInt32 { + // for EPOLLIN/EPOLLOUT + var rawValue: UInt32 { + self + } +} + +extension Int32 { + var rawValue: UInt32 { + UInt32(bitPattern: self) + } +} +#elseif canImport(Musl) +extension EPOLL_EVENTS { + init(_ rawValue: Int32) { + self.init(UInt32(bitPattern: rawValue)) + } +} + +extension Int32 { + // for EPOLLIN/EPOLLOUT + var rawValue: UInt32 { + UInt32(bitPattern: self) + } +} +#endif + extension Configuration { internal func spawn( withInput inputPipe: consuming CreatedPipe, @@ -200,7 +245,7 @@ public struct ProcessIdentifier: Sendable, Hashable { internal func close() { if self.processDescriptor > 0 { - _SubprocessCShims.close(self.processDescriptor) + _ = _subprocess_close(self.processDescriptor) } } } @@ -373,6 +418,10 @@ internal func monitorProcessTermination( } } +#if canImport(Musl) +extension pthread_t: @retroactive @unchecked Sendable { } +#endif + private enum ProcessMonitorState { struct Storage { let epollFileDescriptor: CInt @@ -386,7 +435,7 @@ private enum ProcessMonitorState { case failed(SubprocessError) } -private final class MonitorThreadContext { +private struct MonitorThreadContext: Sendable { let epollFileDescriptor: CInt let shutdownFileDescriptor: CInt @@ -440,7 +489,7 @@ private func shutdown() { var one: UInt64 = 1 // Wake up the thread for shutdown - _ = _SubprocessCShims.write(storage.shutdownFileDescriptor, &one, MemoryLayout.size) + _ = _subprocess_write(storage.shutdownFileDescriptor, &one, MemoryLayout.size) // Cleanup the monitor thread pthread_join(storage.monitorThread, nil) } @@ -456,14 +505,11 @@ private func signalHandler( ) { let savedErrno = errno var one: UInt8 = 1 - _SubprocessCShims.write(_signalPipe.writeEnd, &one, 1) + _ = _subprocess_write(_signalPipe.writeEnd, &one, 1) errno = savedErrno } -private func monitorThreadFunc(args: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { - let unmanaged = Unmanaged.fromOpaque(args!) - let context = unmanaged.takeRetainedValue() - +private func monitorThreadFunc(context: MonitorThreadContext) { var events: [epoll_event] = Array( repeating: epoll_event(events: 0, data: epoll_data(fd: 0)), count: 256 @@ -515,7 +561,7 @@ private func monitorThreadFunc(args: UnsafeMutableRawPointer?) -> UnsafeMutableR // from the shutdownFD if targetFileDescriptor == context.shutdownFileDescriptor { var buf: UInt64 = 0 - _ = _SubprocessCShims.read(context.shutdownFileDescriptor, &buf, MemoryLayout.size) + _ = _subprocess_read(context.shutdownFileDescriptor, &buf, MemoryLayout.size) break monitorLoop } @@ -527,8 +573,6 @@ private func monitorThreadFunc(args: UnsafeMutableRawPointer?) -> UnsafeMutableR } } } - - return nil } private let setup: () = { @@ -618,10 +662,24 @@ private let setup: () = { epollFileDescriptor: epollFileDescriptor, shutdownFileDescriptor: shutdownFileDescriptor ) - let unmanagedContext = Unmanaged.passRetained(monitorThreadContext) // Create the monitor thread - var thread = pthread_t() - pthread_create(&thread, nil, monitorThreadFunc, unmanagedContext.toOpaque()) + let thread: pthread_t + switch Result(catching: { () throws(SubprocessError.UnderlyingError) -> pthread_t in + try pthread_create { + monitorThreadFunc(context: monitorThreadContext) + } + }) { + case let .success(t): + thread = t + case let .failure(error): + _processMonitorState.withLock { state in + state = .failed(SubprocessError( + code: .init(.failedToMonitorProcess), + underlyingError: error + )) + } + return + } let storage = ProcessMonitorState.Storage( epollFileDescriptor: epollFileDescriptor, @@ -707,7 +765,7 @@ private func _reapAllKnownChildProcesses(_ signalFd: CInt, context: MonitorThrea // Drain the signalFd var buffer: UInt8 = 0 - while _SubprocessCShims.read(signalFd, &buffer, 1) > 0 { /* noop, drain the pipe */ } + while _subprocess_read(signalFd, &buffer, 1) > 0 { /* noop, drain the pipe */ } let resumingContinuations: [ResultContinuation] = _processMonitorState.withLock { state in guard case .started(let storage) = state else { @@ -780,5 +838,43 @@ internal func _isWaitprocessDescriptorSupported() -> Bool { return errno == ECHILD } +internal func pthread_create(_ body: @Sendable @escaping () -> ()) throws(SubprocessError.UnderlyingError) -> pthread_t { + final class Context { + let body: @Sendable () -> () + init(body: @Sendable @escaping () -> Void) { + self.body = body + } + } + #if canImport(Glibc) || canImport(Musl) + func proc(_ context: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { + (Unmanaged.fromOpaque(context!).takeRetainedValue() as! Context).body() + return nil + } + #elseif canImport(Bionic) + func proc(_ context: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + (Unmanaged.fromOpaque(context).takeRetainedValue() as! Context).body() + return context + } + #endif + #if canImport(Glibc) || canImport(Bionic) + var thread = pthread_t() + #else + var thread: pthread_t? + #endif + let rc = pthread_create( + &thread, + nil, + proc, + Unmanaged.passRetained(Context(body: body)).toOpaque() + ) + if rc != 0 { + throw SubprocessError.UnderlyingError(rawValue: rc) + } + #if canImport(Glibc) || canImport(Bionic) + return thread + #else + return thread! + #endif +} #endif // canImport(Glibc) || canImport(Android) || canImport(Musl) diff --git a/Sources/Subprocess/Platforms/Subprocess+Unix.swift b/Sources/Subprocess/Platforms/Subprocess+Unix.swift index 8b35139..555ca90 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Unix.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Unix.swift @@ -180,7 +180,7 @@ extension Environment { /// length = `key` + `=` + `value` + `\null` let totalLength = keyContainer.count + 1 + valueContainer.count + 1 let fullString: UnsafeMutablePointer = .allocate(capacity: totalLength) - #if os(Linux) + #if os(Linux) || os(Android) _ = _shims_snprintf(fullString, CInt(totalLength), "%s=%s", rawByteKey, rawByteValue) #else _ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue) diff --git a/Sources/_SubprocessCShims/process_shims.c b/Sources/_SubprocessCShims/process_shims.c index eaf98db..906f70a 100644 --- a/Sources/_SubprocessCShims/process_shims.c +++ b/Sources/_SubprocessCShims/process_shims.c @@ -17,7 +17,7 @@ // For pidfd_open #include #include -#include +#include #endif #include "include/process_shims.h" @@ -316,13 +316,28 @@ int _pidfd_open(pid_t pid) { } // SYS_clone3 is only defined on Linux Kernel 5.3 and above -// Define our dummy value if it's not available +// Define our dummy value if it's not available (as is the case with Musl libc) #ifndef SYS_clone3 -#define SYS_clone3 0xFFFF +#define SYS_clone3 435 #endif +// Can't use clone_args from sched.h because only Glibc defines it; Musl does not (and there's no macro to detect Musl) +struct _subprocess_clone_args { + uint64_t flags; + uint64_t pidfd; + uint64_t child_tid; + uint64_t parent_tid; + uint64_t exit_signal; + uint64_t stack; + uint64_t stack_size; + uint64_t tls; + uint64_t set_tid; + uint64_t set_tid_size; + uint64_t cgroup; +}; + static int _clone3(int *pidfd) { - struct clone_args args = { + struct _subprocess_clone_args args = { .flags = CLONE_PIDFD, // Get a pidfd referring to child .pidfd = (uintptr_t)pidfd, // Int pointer for the pidfd (int pidfd = -1;) .exit_signal = SIGCHLD, // Ensure parent gets SIGCHLD diff --git a/Tests/SubprocessTests/SubprocessTests+Linux.swift b/Tests/SubprocessTests/SubprocessTests+Linux.swift index f4e87f2..0fb6718 100644 --- a/Tests/SubprocessTests/SubprocessTests+Linux.swift +++ b/Tests/SubprocessTests/SubprocessTests+Linux.swift @@ -11,8 +11,9 @@ #if os(Linux) || os(Android) -#if canImport(Bionic) -import Bionic +#if canImport(Android) +import Android +import Foundation #elseif canImport(Glibc) import Glibc #elseif canImport(Musl)