Last active
March 7, 2026 09:23
-
-
Save JJTech0130/cb08ab3f458d7ce7f943abed54a2e275 to your computer and use it in GitHub Desktop.
Using USB passthrough with Virtualization.framework
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Dynamic | |
| import Foundation | |
| import IOKit | |
| import Virtualization | |
| enum PassthroughError: Error { | |
| case deviceNotFound(vendor: Int, product: Int) | |
| case failedToCreateDeviceConfig(underlyingError: Error?) | |
| case failedToCreateDevice(underlyingError: Error?) | |
| case failedToAttachDevice(underlyingError: Error) | |
| } | |
| /// Waits for a USB device to appear in IOKit and attaches it to the given USB controller. | |
| @MainActor | |
| func attachUSBDeviceToController( | |
| _ controller: VZUSBController, | |
| vendor: Int, | |
| product: Int, | |
| ) async throws { | |
| // Patch buggy VZIOUSBHostPassthroughDevice behavior on Sequoia | |
| if #available(macOS 15, *), ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 15 { | |
| VZSequoiaSwizzle.install() | |
| } | |
| let label = String(format: "vendor=0x%04x product=0x%04x", vendor, product) | |
| // poll every 0.5s for the device to show up in IOKit, with a 10s timeout | |
| let deadline = Date().addingTimeInterval(10) | |
| var service: io_service_t? | |
| print("attachUSBDeviceToController: waiting for device with \(label) to appear in IOKit") | |
| repeat { | |
| let matching = IOServiceMatching("IOUSBHostDevice") as NSMutableDictionary | |
| matching["idVendor"] = vendor | |
| matching["idProduct"] = product | |
| let svc = IOServiceGetMatchingService(kIOMainPortDefault, matching) | |
| if svc != IO_OBJECT_NULL { | |
| service = svc | |
| break | |
| } | |
| try? await Task.sleep(nanoseconds: 500_000_000) | |
| } while Date() < deadline | |
| guard let service else { | |
| throw PassthroughError.deviceNotFound(vendor: vendor, product: product) | |
| } | |
| defer { IOObjectRelease(service) } | |
| print("attachUSBDeviceToController: found device with \(label) in IOKit, creating config") | |
| var initErr: NSError? | |
| let deviceConfig = Dynamic._VZIOUSBHostPassthroughDeviceConfiguration | |
| .initWithService(service, error: &initErr).asObject | |
| guard let deviceConfig = deviceConfig as? VZUSBDeviceConfiguration else { | |
| throw PassthroughError.failedToCreateDeviceConfig(underlyingError: initErr) | |
| } | |
| let device = Dynamic._VZIOUSBHostPassthroughDevice | |
| .initWithConfiguration(deviceConfig, error: &initErr).asObject | |
| guard let device = device as? VZUSBDevice else { | |
| throw PassthroughError.failedToCreateDevice(underlyingError: initErr) | |
| } | |
| print("attachUSBDeviceToController: attaching \(label) to VM") | |
| do { | |
| try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in | |
| controller.attach(device: device) { error in | |
| if let error { | |
| cont.resume(throwing: error) | |
| } else { | |
| cont.resume() | |
| } | |
| } | |
| } | |
| print("attachUSBDeviceToController: successfully attached \(label) to VM") | |
| } catch { | |
| throw PassthroughError.failedToAttachDevice(underlyingError: error) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import AppKit | |
| import Foundation | |
| import ObjectiveC | |
| let noop: @convention(c) (AnyObject, Selector) -> Void = { _, _ in } | |
| public enum VZUSBSequoiaSwizzle { | |
| public static func install() { | |
| noopCursorHide() | |
| noopNotifierLock() | |
| } | |
| // VzCore::Hardware::Usb::Darwin::usb_device_service_has_hid_pointing_device_interface incorrectly identifies | |
| // all HID devices as pointing devices, which causes the view to hide the cursor | |
| // HACK: swizzle all [NSCursor hide] calls (even legitimate ones...) | |
| private static func noopCursorHide() { | |
| guard let m = class_getClassMethod(NSCursor.self, NSSelectorFromString("hide")) else { | |
| return | |
| } | |
| method_setImplementation(m, unsafeBitCast(noop, to: IMP.self)) | |
| } | |
| // IOUSBHostInterestNotifier uses an NSRecursiveLock to serialise interest notification delivery | |
| // Something that calls [IOUSBHostInterestNotifier destroy] which acquires that lock and causes a deadlock | |
| // HACK: simply swizzle it to return a fake lock object | |
| private static func noopNotifierLock() { | |
| guard let cls = NSClassFromString("IOUSBHostInterestNotifier"), | |
| let parentCls = NSClassFromString("NSRecursiveLock") | |
| else { return } | |
| for sel in [NSSelectorFromString("lock"), NSSelectorFromString("unlock")] { | |
| let enc = class_getInstanceMethod(parentCls, sel).flatMap { method_getTypeEncoding($0) } | |
| if !class_addMethod(cls, sel, unsafeBitCast(noop, to: IMP.self), enc), | |
| let m = class_getInstanceMethod(cls, sel) | |
| { | |
| method_setImplementation(m, unsafeBitCast(noop, to: IMP.self)) | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment