Skip to content

Instantly share code, notes, and snippets.

@JJTech0130
Last active March 7, 2026 09:23
Show Gist options
  • Select an option

  • Save JJTech0130/cb08ab3f458d7ce7f943abed54a2e275 to your computer and use it in GitHub Desktop.

Select an option

Save JJTech0130/cb08ab3f458d7ce7f943abed54a2e275 to your computer and use it in GitHub Desktop.
Using USB passthrough with Virtualization.framework
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)
}
}
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