# Recipe: handing a `lease.raw()` fd to libusb

`Client.claim(urn).raw()` returns a `dict` carrying `fd` — an
SCM_RIGHTS-passed kernel handle on the device's USBDEVFS open-file.
For libusb-1.0 consumers (libimobiledevice, libusbmuxd, libirecovery,
idevicerestore, tsschecker, futurerestore, palera1n, any Android
fastboot tool — they're all libusb-1.0 underneath) the question is:
*how do I use that fd with my existing library?*

This recipe answers it. The handoff is **one libusb call**
(`libusb_wrap_sys_device`); the contract is **one paragraph**
(broker owns the lease, you own the fd, neither closes the kernel
handle out from under the other). Get those two things right and
the broker's G4 dividend lands — claim once, the broker steps out,
you get full-speed direct USB access.

> **What this requires.** mgf-hrb v0.1.x ships `Client.claim()` and
> `lease.raw()` end-to-end via the FakeBackend. Real USB devices land
> when [USB-02](../FEEDBACK.md) ships the USB worker's claim path
> (Slice B). The libusb call shape works against either today.
>
> **libusb version.** `libusb_wrap_sys_device` requires
> libusb-1.0.23 (Oct 2020) or newer. Every supported distro since
> Ubuntu 20.04, Debian 11, Fedora 33, RHEL 9 ships ≥1.0.23. If you
> need libusb <1.0.23 (Ubuntu 18.04, ancient embedded), see §5.

---

## §1. The pattern

Three lines once the broker has handed you the fd:

```python
import libusb1                       # pip install libusb1
import usb1                          # the higher-level wrapper

from mgf.hrb import Client, ClaimMode

with Client.connect("/tmp/mgf-hrb.sock") as c:
    with c.claim("hw:/usb/bus3/port-9/dev/if0", ClaimMode.EXCLUSIVE) as lease:
        ch = lease.raw()
        fd: int = ch["fd"]

        # The handoff. libusb takes the kernel handle as-is —
        # no second `open()`, no driver detach, no re-enumeration.
        ctx = usb1.USBContext()
        ctx.setOption(libusb1.LIBUSB_OPTION_NO_DEVICE_DISCOVERY)
        handle = ctx.wrapSysDevice(fd)

        # ...drive the device via libusb...
        handle.controlWrite(...)
        handle.bulkWrite(...)

        # NOTE: do NOT call handle.close() OR os.close(fd).
        # The lease's context-manager exit releases the broker's
        # side; the fd's kernel reference disappears with it.
        # Calling close() yourself races the broker's tracking.
```

That's the entire happy path. Sections below cover what each piece
is doing and the failure modes.

---

## §2. Why `libusb_wrap_sys_device`, not `libusb_open`

`libusb_open(device_handle*, **handle)` is the default path: libusb
opens the device's kernel file itself and tracks the resulting fd.
The trouble: **the broker already opened it**. Calling
`libusb_open` would either (a) get `LIBUSB_ERROR_BUSY` because the
kernel sees the open-file is held, or (b) succeed and break the
broker's lease invariant (two userspace owners of the same kernel
fd is exactly what the broker exists to prevent).

`libusb_wrap_sys_device(ctx, fd, **handle)` (libusb-1.0.23+) is the
fd-passing entry point. It takes an existing kernel handle and
returns a `libusb_device_handle*` that drives it. **The broker
keeps owning the lease; libusb just borrows the fd to issue
ioctls.**

The matching Python flavour via the `libusb1` package is
`usb1.USBContext.wrapSysDevice(fd)`. Same shape in C, libusb-1.0
bindings for other languages, Rust's `rusb` (>=0.9), etc.

**`LIBUSB_OPTION_NO_DEVICE_DISCOVERY`.** When you set this option
before any libusb operation, libusb skips the enumeration step
that would otherwise scan `/dev/bus/usb/` and try to match your fd
against an enumerated device. Skipping that work is faster AND
avoids a race where the broker is mid-claim on a sibling device.

---

## §3. The ownership contract

| Side | Owns | MUST NOT |
|---|---|---|
| **broker** | the lease's liveness; the kernel open-file's *existence* | close the kernel handle while you hold the lease |
| **consumer (you)** | issuing ioctls / transfers via the libusb handle | close the fd; close the libusb handle; call `libusb_release_interface` (the broker tracks claims separately) |

The asymmetry: **the fd is yours to USE; the kernel handle's
lifetime is the broker's to MANAGE.** The lease's
`__enter__/__exit__` (or `lease.release()`) drives that lifetime.
Forgetting this is the load-bearing mistake.

Three concrete consequences:

1. **Do not `os.close(fd)` yourself.** The broker's revoke path
   closes the fd at release time. An early close races the broker's
   tracking — exact-failure-mode depends on the kernel scheduler.
   Symptom: revoked-mid-call ioctl returns `EBADF` from libusb
   instead of the typed lease error.

2. **Do not `handle.close()` (`libusb_close(handle)`).** Same
   reason. Drop the Python reference to `handle` (let GC clean it)
   or let the `USBContext` exit; both leave the underlying fd
   alone.

3. **Do not call `libusb_release_interface(handle, N)`.** The
   broker's lease covers the interface claim already. Releasing
   from your side desynchronises the broker's view; the next
   ioctl could be either silently re-claimed by another tenant or
   refused.

The flip side: **the broker's release WILL invalidate your fd.**
After `lease.release()` (explicit or context-exit), any libusb
call on the fd returns `LIBUSB_ERROR_NO_DEVICE`. That's the
expected behaviour, not a bug.

---

## §4. Failure modes

| Symptom | Cause | Fix |
|---|---|---|
| `wrapSysDevice` returns `LIBUSB_ERROR_NOT_SUPPORTED` | libusb <1.0.23, or the fd is not a USBDEVFS handle (broker handed you a VFIO fd instead — the URN was `hw:/pci/...`, not `hw:/usb/...`) | Check libusb version (`libusb_get_version()`); confirm the URN's family |
| All libusb calls return `LIBUSB_ERROR_NO_DEVICE` immediately after wrap | broker already revoked the lease (likely a `Client.revoke()` from another tenant, or the device was unplugged) | Check `RevokedError` on the original `claim()`; subscribe to events to see the revoke notification |
| `controlWrite` returns `LIBUSB_ERROR_BUSY` | another userspace process opened the device after the broker (race with a daemon — see [USB-03 — usbmuxd coexistence](../FEEDBACK.md)) | Stop the conflicting daemon (`sudo systemctl stop usbmuxd` for iFig devices); retry the claim |
| `bulkTransfer` succeeds but the device behaves wrongly | the broker held the device through a mode change that renumbered interfaces; you're talking to the wrong interface number | After a `state_changed` event, `release()` + re-`claim()` on the new URN. See [track-a-device-across-a-mode-change.md](track-a-device-across-a-mode-change.md) |
| Process hang on `lease.release()` | a libusb transfer is in-flight on the fd | Cancel transfers before letting the lease exit; libusb's `cancelAllTransfers` on the handle |

---

## §5. Older libusb (<1.0.23) — the fallback

If you genuinely cannot upgrade libusb, the alternative is to
**not use libusb on the broker's fd**. Two patterns:

1. **Use the kernel ioctls directly.** USBDEVFS exposes
   `USBDEVFS_CONTROL`, `USBDEVFS_BULK`, etc. via `ioctl(fd, ...)`.
   Python's `fcntl.ioctl` works. Use `libirecovery`'s ioctl shape
   as a worked example — every libusb-1.0 caller is one level of
   indirection above this.

2. **Don't use the broker's fd; use the broker's URN as a fence.**
   Stop the conflicting daemon (so libusb_open works against the
   device), then call `Client.claim(urn, ClaimMode.OBSERVE)` to
   keep the broker's view consistent. You drive the device via
   libusb's own enumeration. This loses the G4 dividend (your
   process is the lifetime owner of the kernel handle, not the
   broker) but works on libusb 1.0.21.

Most consumers should upgrade libusb instead. The version is six
years old at this point.

---

## §6. Worked example: handing a broker fd to libirecovery

`libirecovery`'s `irecv_open_with_ecid` opens an iFig device by
ECID, calling `libusb_open` internally. There's no upstream API
for "take an existing fd". Two ways forward:

**A. PR / patch libirecovery to add `irecv_open_from_fd(fd, ecid)`.**

```c
// Sketch — add to libirecovery/include/libirecovery.h
irecv_error_t irecv_open_from_fd(int fd, uint64_t ecid,
                                  irecv_client_t *client);

// libirecovery/src/libirecovery.c
irecv_error_t irecv_open_from_fd(int fd, uint64_t ecid,
                                  irecv_client_t *client) {
    libusb_context *ctx;
    libusb_init(&ctx);
    libusb_set_option(ctx, LIBUSB_OPTION_NO_DEVICE_DISCOVERY);
    libusb_device_handle *handle;
    int rc = libusb_wrap_sys_device(ctx, (intptr_t)fd, &handle);
    if (rc != LIBUSB_SUCCESS) return IRECV_E_NO_DEVICE;
    // ... allocate irecv_client_t, attach handle + ecid ...
    return IRECV_E_SUCCESS;
}
```

This is a one-time shim per upstream library. Worth upstreaming
because every libusb-1.0 wrapper hits the same wall.

**B. Hold the fd; let libirecovery enumerate against the broker
device anyway.** If you've already stopped `usbmuxd`, libirecovery's
default `irecv_open_with_ecid` will work — but you lose the G4
dividend (libirecovery owns the kernel handle, not the broker).
This is the v0.x baseline; flip to A once a real consumer's
restore flow stresses crash-safety.

---

## §7. Cross-references

- [FEEDBACK paper DOC-03](../FEEDBACK.md) — this recipe's filing.
- [FEEDBACK paper USB-02 Slice B](../FEEDBACK.md) — the USB-worker
  slice that ships the real `lease.raw()` for USB URNs.
- [FEEDBACK paper USB-03](../FEEDBACK.md) — the usbmuxd-coexistence
  question (what happens when a daemon already holds the device).
- [recipes/track-a-device-across-a-mode-change.md](track-a-device-across-a-mode-change.md)
  — what to do after a `state_changed` event invalidates the lease.
- [recipes/claim-a-device-for-vfio-passthrough.md](claim-a-device-for-vfio-passthrough.md)
  — the PCI / VFIO sibling pattern (different fd shape, same
  ownership contract).
- libusb 1.0 docs: [`libusb_wrap_sys_device`](https://libusb.sourceforge.io/api-1.0/group__libusb__dev.html)
- python-libusb1 package: <https://pypi.org/project/libusb1/>
