Skip to content

USB: claim mass-storage devices from a privileged helper (proposal) - #7842

Open
giovariot wants to merge 1 commit into
utmapp:mainfrom
giovariot:utm-usb-helper
Open

USB: claim mass-storage devices from a privileged helper (proposal)#7842
giovariot wants to merge 1 commit into
utmapp:mainfrom
giovariot:utm-usb-helper

Conversation

@giovariot

Copy link
Copy Markdown

Draft / RFC. This is a design proposal with a working reference implementation, not a merge-ready patch. I am opening it because the problem it solves comes up regularly and the diagnosis took a while to pin down — even if you decide not to take this approach, the findings may save someone else the same days.

The problem

On macOS, USB redirection cannot take over a mass-storage device. libusb_claim_interface() returns LIBUSB_ERROR_ACCESS for any device already bound to a kernel driver, so the disk either does not appear in the guest or appears without a working data path.

Measured with a controlled A/B comparison — same device, same unmount, same binary, only the uid changes:

  • as a normal user: USB device capture requires either an entitlement (com.apple.vm.device-access) or root privilege
  • as root: the claim succeeds

What this adds

A small privileged LaunchDaemon, UTMUSBHelper, registered through SMAppService (macOS 13+). It performs the claim, speaks to the app over XPC, and pumps the traffic through libusbredirhost. QEMU keeps running as an ordinary user process and only ever sees a socket — nothing changes for it regarding TCC, storage locations or the group container.

Two invariants the code enforces, both found the hard way:

  • the device's volumes are unmounted before the claim — never in parallel, never afterwards. A device claimed while macOS still has it mounted means two owners for the same disk.
  • on release the device is reset to force re-enumeration, even when the claim failed. Without it the device stays enumerated on the bus with no block device, diskutil hangs, and the only way out for the user is unplugging the cable.

The honest caveat

Root is not the whole story: TCC is. Claiming a USB interface on a storage device goes through the iokit-open-service IOUSBHostInterface sandbox gate, which consults TCC. For a root process TCC also asks the system domain, and that domain can only answer if it can attribute the request to a responsible process that is a user-session app with Full Disk Access. A LaunchDaemon has no such responsible process, whatever its uid: the question becomes RemovableVolumes, TCC tries to forward it to a user agent it cannot reach (bootstrap look-up: No such process) and denies — silently.

So this daemon works as designed only with the com.apple.vm.device-access entitlement, which is restricted. With a signing identity that has it, this is the clean shape: registration once, lifetime managed by launchd, no per-launch authorization prompt.

Without that entitlement there is a workaround — having the app itself spawn the privileged worker via Authorization Services, so the worker inherits a responsible process in the user session and TCC does show the prompt. That variant is what I currently ship downstream; it is uglier (an authorization prompt, a root process spawned by the app) and I did not propose it here.

State of the patch

  • extracted from a downstream fork (UniversEmu) and renamed to UTM conventions, but not wired into the Xcode project: a UTMUSBHelper target and a copy phase into Contents/Library/LaunchDaemons/ are still needed
  • file-level comments are in English; some inline comments are still in Italian, and I will translate them if there is interest in taking this further
  • happy to rework it in whatever direction you prefer, or to close it and leave it as a reference

USB redirection currently cannot take over a mass-storage device on macOS:
`libusb_claim_interface()` returns LIBUSB_ERROR_ACCESS for any device already
bound to a kernel driver, so the disk shows up in the guest without its data
path, or not at all.

This adds a small privileged LaunchDaemon (`UTMUSBHelper`) that does the
claim, talks to the app over XPC, and hands the traffic to libusbredirhost.
QEMU keeps running as a normal user process and only sees a socket, so
nothing about TCC, storage locations or the group container changes for it.

Measured with a controlled A/B comparison (same device, same unmount, same
binary, only the uid changes): as a normal user libusb refuses with "USB
device capture requires either an entitlement (com.apple.vm.device-access)
or root privilege"; as root it succeeds.

Two invariants the code enforces, both found the hard way:

  * the volumes of the device are unmounted BEFORE the claim, never in
    parallel and never after — a device claimed while macOS still has it
    mounted means two owners for the same disk;
  * on release the device is reset to force re-enumeration, even when the
    claim failed — without it the device stays on the bus with no block
    device, `diskutil` hangs, and the only way out for the user is pulling
    the cable.

Not merge-ready as it stands: see the pull request description.
@giovariot
giovariot marked this pull request as ready for review August 31, 2026 22:23
@osy

osy commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Please follow https://github.com/utmapp/UTM/blob/main/CONTRIBUTING.md specifically you must use /utm-review and /utm-submit and also state what platform configuration you've tested on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants