From 63ee7fd138a25d4b4bb72dc264b3d096e11d9025 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 10:01:10 +1100 Subject: [PATCH 1/8] update IORingSwift to 1.0.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 0956684..c4b24bc 100644 --- a/Package.swift +++ b/Package.swift @@ -39,7 +39,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/PADL/IORingSwift", branch: "main"), + .package(url: "https://github.com/PADL/IORingSwift", from: "1.0.0"), .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), .package(url: "https://github.com/apple/swift-system", from: "1.2.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"), From d1ef099ff16d901f155e12af2fa3f691a1eecce1 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 09:52:13 +1100 Subject: [PATCH 2/8] fix: serialize nl_sock access through a dedicated DispatchQueue nl_sock is not thread-safe. The DispatchSource read handler, nl_send_auto, and nl_socket_use_seq were previously called from different queues concurrently. Route all nl_sock access through a single serial DispatchQueue to prevent data races. --- Sources/NetLink/NetLink.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/NetLink/NetLink.swift b/Sources/NetLink/NetLink.swift index 0b902ab..7fc00af 100644 --- a/Sources/NetLink/NetLink.swift +++ b/Sources/NetLink/NetLink.swift @@ -248,6 +248,7 @@ Sendable { } let _sk: OpaquePointer! + let _queue = DispatchQueue(label: "NLSocket") private let _readSource: any DispatchSourceRead private let _requests = Mutex<[UInt32: _Request]>([:]) @@ -266,7 +267,7 @@ Sendable { let fd = nl_socket_get_fd(sk) precondition(fd >= 0) - _readSource = DispatchSource.makeReadSource(fileDescriptor: fd, queue: .main) + _readSource = DispatchSource.makeReadSource(fileDescriptor: fd, queue: _queue) _readSource.setEventHandler(handler: onReadReady) nl_socket_modify_cb( @@ -344,13 +345,15 @@ Sendable { } public func useNextSequenceNumber() -> UInt32 { - var nextSequenceNumber: UInt32 + _queue.sync { + var nextSequenceNumber: UInt32 - repeat { - nextSequenceNumber = nl_socket_use_seq(_sk) - } while nextSequenceNumber == 0 + repeat { + nextSequenceNumber = nl_socket_use_seq(_sk) + } while nextSequenceNumber == 0 - return nextSequenceNumber + return nextSequenceNumber + } } private func _lookup(sequence: UInt32, forceRemove: Bool) -> _Request? { @@ -858,7 +861,9 @@ struct NLMessage: ~Copyable { } func send(on socket: NLSocket) throws { - try throwingNLError { nl_send_auto(socket._sk, _msg) } + try socket._queue.sync { + try throwingNLError { nl_send_auto(socket._sk, _msg) } + } } deinit { From f1842bcfdc0351aa60776ef5cee12611901d880c Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 09:52:57 +1100 Subject: [PATCH 3/8] fix: replace AsyncThrowingChannel with AsyncThrowingStream for notifications Using AsyncThrowingChannel required spawning an unstructured Task to call the async send() method from a synchronous callback, which destroyed kernel event ordering. Switch to AsyncThrowingStream with an unbounded buffering policy so yield() can be called synchronously, preserving strict event ordering. --- Sources/NetLink/NetLink.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/NetLink/NetLink.swift b/Sources/NetLink/NetLink.swift index 7fc00af..c5ae50b 100644 --- a/Sources/NetLink/NetLink.swift +++ b/Sources/NetLink/NetLink.swift @@ -230,7 +230,7 @@ Sendable { private typealias Continuation = CheckedContinuation private typealias Stream = AsyncThrowingStream private typealias Ack = CheckedContinuation<(), Error> - public typealias Channel = AsyncThrowingChannel + public typealias NotificationStream = AsyncThrowingStream private enum _Request { case continuation(Continuation) @@ -252,9 +252,14 @@ Sendable { private let _readSource: any DispatchSourceRead private let _requests = Mutex<[UInt32: _Request]>([:]) - public let notifications = Channel() + private let _notificationsContinuation: NotificationStream.Continuation + public let notifications: NotificationStream public init(protocol: Int32) throws { + var continuation: NotificationStream.Continuation! + notifications = NotificationStream(bufferingPolicy: .unbounded) { continuation = $0 } + _notificationsContinuation = continuation + guard let sk = nl_socket_alloc() else { throw NLError.noMemory } nl_socket_disable_seq_check(sk) _sk = sk @@ -303,6 +308,7 @@ Sendable { deinit { _readSource.cancel() + _notificationsContinuation.finish() nl_socket_free(_sk) } @@ -408,13 +414,7 @@ Sendable { } } } else { - Task { - do { - try await notifications.send(result.get()) - } catch { - notifications.fail(error) - } - } + _notificationsContinuation.yield(with: result) } } From e5111dfc67babc428031ef4648157d4fff098204 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 09:53:14 +1100 Subject: [PATCH 4/8] fix: handle zero/multiple callbacks in NLObject.init(msg:) If nl_msg_parse's callback fires zero times, obj remains nil and would crash when passed to self.init(obj:). If it fires multiple times, the previous pointer was leaked. Now release the previous object before overwriting, and throw NLError.invalidArgument if no object was parsed. --- Sources/NetLink/NetLink.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/NetLink/NetLink.swift b/Sources/NetLink/NetLink.swift index c5ae50b..b94115f 100644 --- a/Sources/NetLink/NetLink.swift +++ b/Sources/NetLink/NetLink.swift @@ -73,18 +73,24 @@ Sendable, Equatable, Hashable, CustomStringConvertible { try withUnsafeMutablePointer(to: &obj) { objRef in _ = try throwingNLError { nl_msg_parse(msg, { obj, objRef in - nl_object_get(obj) - objRef! + let ptr = objRef! .withMemoryRebound( - to: OpaquePointer.self, + to: OpaquePointer?.self, capacity: 1 - ) { objRef in - objRef.pointee = obj! - } + ) { $0 } + if let existing = ptr.pointee { + nl_object_put(existing) + } + nl_object_get(obj) + ptr.pointee = obj }, objRef) } } + guard obj != nil else { + throw NLError.invalidArgument + } + self.init(obj: obj, constructFromObject: constructFromObject) nl_object_put(obj) } From 03419770c3476d1434cbb353ec895aa59b853ce9 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 09:53:25 +1100 Subject: [PATCH 5/8] fix: guard nil return from nfnl_log_alloc() nfnl_log_alloc() can return nil on OOM. Passing nil to NLObject(consumingObj:) which expects a non-optional OpaquePointer would crash. Guard and throw NLError.noMemory instead. --- Sources/NetLink/NFNetLink.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/NetLink/NFNetLink.swift b/Sources/NetLink/NFNetLink.swift index 55b9323..c210406 100644 --- a/Sources/NetLink/NFNetLink.swift +++ b/Sources/NetLink/NFNetLink.swift @@ -140,7 +140,8 @@ public final class NFNLLog: Sendable { public init(family: sa_family_t = sa_family_t(AF_BRIDGE), group: UInt16) throws { _socket = try NLSocket(protocol: NETLINK_NETFILTER) - _log = NLObject(consumingObj: nfnl_log_alloc()) + guard let logObj = nfnl_log_alloc() else { throw NLError.noMemory } + _log = NLObject(consumingObj: logObj) try throwingNLError { nfnl_log_pf_bind(_socket._sk, UInt8(family)) } From ceb833b4b67fa5db57cbb66325c329396f42d0e5 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 09:53:38 +1100 Subject: [PATCH 6/8] fix: clean up leaked stream continuation on send failure in streamRequest If message.send() throws, the stream continuation remained in the _requests dictionary forever. Remove it on error to prevent the leak. --- Sources/NetLink/NetLink.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/NetLink/NetLink.swift b/Sources/NetLink/NetLink.swift index b94115f..6d76d03 100644 --- a/Sources/NetLink/NetLink.swift +++ b/Sources/NetLink/NetLink.swift @@ -475,7 +475,12 @@ Sendable { } stream = _stream } - try message.send(on: self) + do { + try message.send(on: self) + } catch { + _requests.withLock { $0.removeValue(forKey: sequence) } + throw error + } return stream } } From 5260286d492da57285d5ea1e097d4acdbc66768d Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 09:53:47 +1100 Subject: [PATCH 7/8] fix: return NL_SKIP from error callback instead of aborting nl_recvmsgs Returning the negative errno from the error callback caused nl_recvmsgs to abort, discarding other pending messages in the receive buffer. Return NL_SKIP to continue processing remaining messages. --- Sources/NetLink/NetLink.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NetLink/NetLink.swift b/Sources/NetLink/NetLink.swift index 6d76d03..be0337d 100644 --- a/Sources/NetLink/NetLink.swift +++ b/Sources/NetLink/NetLink.swift @@ -228,7 +228,7 @@ private func NLSocket_ErrCB( let hdr = err.pointee.msg debugPrint("NLSocket_ErrCB: error \(err.pointee)") nlSocket.yield(sequence: hdr.nlmsg_seq, with: Result.failure(Errno(rawValue: -err.pointee.error))) - return err.pointee.error + return CInt(NL_SKIP.rawValue) } public final class NLSocket: @unchecked From 26cad34778b1e52dfc3ed9c54569e55e6607df8e Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 20 Mar 2026 09:53:59 +1100 Subject: [PATCH 8/8] fix: avoid unnecessary Array allocation in append(opaque:) Pass the UnsafeRawBufferPointer directly to nlmsg_append instead of copying into an intermediate Array. --- Sources/NetLink/NetLink.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/NetLink/NetLink.swift b/Sources/NetLink/NetLink.swift index be0337d..6ddbaca 100644 --- a/Sources/NetLink/NetLink.swift +++ b/Sources/NetLink/NetLink.swift @@ -778,8 +778,10 @@ struct NLMessage: ~Copyable { } func append(opaque value: UnsafePointer) throws { - _ = try withUnsafeBytes(of: value.pointee) { value in - try append(Array(value)) + try withUnsafeBytes(of: value.pointee) { bytes in + try throwingNLError { + nlmsg_append(_msg, UnsafeMutableRawPointer(mutating: bytes.baseAddress), bytes.count, CInt(NLMSG_ALIGNTO)) + } } }