From 4a018ce4bb2ab442f449300e4bdef679b9bf1cb9 Mon Sep 17 00:00:00 2001 From: Fedor Korotkov Date: Tue, 21 Jul 2026 17:12:27 -0400 Subject: [PATCH] Use an owning Softnet control file handle --- Sources/tart/Network/Softnet.swift | 36 +++++++-------------- Tests/TartTests/SoftnetControlFDTests.swift | 10 ++++++ 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Sources/tart/Network/Softnet.swift b/Sources/tart/Network/Softnet.swift index 92e54f1..4159514 100644 --- a/Sources/tart/Network/Softnet.swift +++ b/Sources/tart/Network/Softnet.swift @@ -13,21 +13,20 @@ class Softnet: Network { private let process = Process() private var monitorTask: Task? = nil private let monitorTaskFinished = ManagedAtomic(false) - private var controlFD: Int32? let vmFD: Int32 init(vmMACAddress: String, extraArguments: [String] = [], controlFD: Int32? = nil) throws { - if let controlFD = controlFD { - do { - try Self.validateControlFD(controlFD) - } catch { - close(controlFD) - throw error - } - } + var controlFileHandle: FileHandle? - self.controlFD = controlFD + if let controlFD = controlFD { + guard controlFD > STDERR_FILENO else { + throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be greater than 2") + } + + controlFileHandle = FileHandle(fileDescriptor: controlFD, closeOnDealloc: true) + try Self.validateControlFD(controlFD) + } let fds = UnsafeMutablePointer.allocate(capacity: MemoryLayout.stride * 2) @@ -46,16 +45,12 @@ class Softnet: Network { process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress] + extraArguments process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false) - if let controlFD = controlFD { + if let controlFileHandle = controlFileHandle { process.arguments! += ["--control-fd", String(STDOUT_FILENO)] - process.standardOutput = FileHandle(fileDescriptor: controlFD, closeOnDealloc: false) + process.standardOutput = controlFileHandle } } - deinit { - closeControlFD() - } - static func validateControlFD(_ fd: Int32) throws { guard fd > STDERR_FILENO else { throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be greater than 2") @@ -100,7 +95,7 @@ class Softnet: Network { } func run(_ sema: AsyncSemaphore) throws { - defer { closeControlFD() } + defer { try? (process.standardOutput as? FileHandle)?.close() } try process.run() @@ -116,13 +111,6 @@ class Softnet: Network { } } - private func closeControlFD() { - if let controlFD = controlFD { - close(controlFD) - self.controlFD = nil - } - } - func stop() async throws { if monitorTaskFinished.load(ordering: .sequentiallyConsistent) { // Consume the monitor task's value to ensure the task has finished diff --git a/Tests/TartTests/SoftnetControlFDTests.swift b/Tests/TartTests/SoftnetControlFDTests.swift index 59696bc..25bd6df 100644 --- a/Tests/TartTests/SoftnetControlFDTests.swift +++ b/Tests/TartTests/SoftnetControlFDTests.swift @@ -51,6 +51,16 @@ final class SoftnetControlFDTests: XCTestCase { XCTAssertThrowsError(try Softnet.validateControlFD(STDERR_FILENO)) } + func testStandardDescriptorsRemainOpenWhenInitializationFails() throws { + for fd in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO] { + let flags = fcntl(fd, F_GETFD) + XCTAssertNotEqual(flags, -1) + + XCTAssertThrowsError(try Softnet(vmMACAddress: "02:00:00:00:00:01", controlFD: fd)) + XCTAssertEqual(fcntl(fd, F_GETFD), flags) + } + } + func testControlChannelIsPassedToSoftnetAndVMFDRemainsDatagram() async throws { let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: false)