From 88ce3a64d76031a2f69354974efd15a6fab34337 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha Date: Tue, 9 Dec 2025 17:29:08 +0000 Subject: [PATCH 1/2] Fix socket lifecycle management Motivation: When an `easy_handle` is added to a `multi_handle`, the callback set for `CURLOPT_CLOSESOCKETFUNCTION` constructs a `_MultiHandle` from an unretained reference to `self`. This could cause a crash in `URLSession` if the `_MultiHandle`'s `self` is deinitialised before the callback is called, and the socket will never be closed. _Partially resolves #3813_ Modifications: - Set the `CURLOPT_CLOSESOCKETFUNCTION` callback to do nothing, deferring closing the socket. - Schedule a socket for closure in the cancellation handler of its dispatch sources. Result: Better management of socket lifecycle. Testing: Passing existing tests is sufficient. --- .../URLSession/libcurl/MultiHandle.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift index f1b3ad6a21..ba96cc9814 100644 --- a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift +++ b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift @@ -195,7 +195,7 @@ private extension URLSession._MultiHandle { } /// Removes socket reference from the shared store. If there is work item scheduled, - /// executes it on the current thread. + /// executes it on the current thread. Then, schedules the socket for closure. func endOperation(for socketReference: _SocketReference) { precondition(socketReferences.removeValue(forKey: socketReference.socket) != nil, "No operation associated with the socket") if let workItem = socketReference.workItem, !workItem.isCancelled { @@ -204,6 +204,8 @@ private extension URLSession._MultiHandle { precondition(!socketReference.shouldClose, "Socket close was scheduled, but there is some pending work left") workItem.perform() } + + self.scheduleClose(for: socketReference.socket) } /// Marks this reference to close socket on deinit. This allows us @@ -253,10 +255,9 @@ internal extension URLSession._MultiHandle { // the connection cache is managed by CURL multi_handle, and sockets can actually // outlive easy_handle (even after curl_easy_cleanup call). That's why // socket management lives in _MultiHandle. - try! CFURLSession_easy_setopt_ptr(handle.rawHandle, CFURLSessionOptionCLOSESOCKETDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() try! CFURLSession_easy_setopt_scl(handle.rawHandle, CFURLSessionOptionCLOSESOCKETFUNCTION) { (clientp: UnsafeMutableRawPointer?, item: CFURLSession_socket_t) in - guard let handle = URLSession._MultiHandle.from(callbackUserData: clientp) else { fatalError() } - handle.scheduleClose(for: item) + // Override close(3)/closesocket(3) to keep socket open. + // CFURLSession_socket_t will be scheduled for closure when endOperation(: _SocketReference) is called. return 0 }.asError() From 838b0bfb597ba378a1e69f3debba15079c956dd1 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha Date: Wed, 10 Dec 2025 14:48:04 +0000 Subject: [PATCH 2/2] Correctly schedule socket closure --- .../URLSession/libcurl/MultiHandle.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift index ba96cc9814..29bdac44d3 100644 --- a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift +++ b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift @@ -203,16 +203,11 @@ private extension URLSession._MultiHandle { // we should cancel pending work when unregister action is requested. precondition(!socketReference.shouldClose, "Socket close was scheduled, but there is some pending work left") workItem.perform() - } - - self.scheduleClose(for: socketReference.socket) - } - /// Marks this reference to close socket on deinit. This allows us - /// to extend socket lifecycle by keeping the reference alive. - func scheduleClose(for socket: CFURLSession_socket_t) { - let reference = socketReferences[socket] ?? _SocketReference(socket: socket) - reference.shouldClose = true + /// Marks this reference to close socket on deinit. This allows us + /// to extend socket lifecycle by keeping the reference alive. + socketReference.shouldClose = true + } } /// Schedules work to be performed when an operation ends for the socket,