diff --git a/.ci/check-usb-fixture-bin.sh b/.ci/check-usb-fixture-bin.sh new file mode 100755 index 00000000..5ed6b6cd --- /dev/null +++ b/.ci/check-usb-fixture-bin.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Keep the USB loopback fixture out of the shipped binary by construction. +# +# USB_LOOPBACK_FIXTURE=1 swaps src/syscall/usbdev-fixture-stub.c for +# src/syscall/usbdev-fixture.c (the SRCS block in the top-level Makefile). That +# changes SRCS, not CFLAGS, and the stale-object guard in mk/common.mk is keyed +# on $(strip $(CFLAGS)) alone, so it has nothing to say about the switch: while +# both flavors linked to build/elfuse, building one and then asking for the +# other printed "Nothing to be done for 'elfuse'" and handed back whichever had +# been linked last. Measured before the split below, on the tree as it stood +# then: make clean; make elfuse; make USB_LOOPBACK_FIXTURE=1 elfuse; make elfuse +# left _usbdev_fixture_lock in build/elfuse until make clean. No byte counts +# here on purpose. That build needed both flavors to write one path, so it +# cannot be produced again to re-measure, and the pair this comment used to +# quote had gone stale against the tree twice over. The dated figures are in +# docs/internals.md; what reproduces today is what the check below asserts. +# +# What keeps them apart now is the path: mk/config.mk points ELFUSE_BIN at +# $(ELFUSE_LOOPBACK_BIN) when the fixture is asked for, so the two flavors never +# write the same file and the shipped one cannot be a stale copy of the other. +# That is one variable, in a file nothing stops a later change from +# re-simplifying, which is what this check is for. It asks make itself rather +# than reading the makefile, so a change that moves the decision elsewhere is +# still covered as long as the answer stays right. +# +# Cheap on purpose: two variable expansions, no compilation. The whole-tree +# reproduction above is what it stands in for. + +set -e -u -o pipefail + +MAKE_BIN="${MAKE:-make}" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +plain="$("$MAKE_BIN" -C "$ROOT" -s --no-print-directory print-elfuse-bin)" +fixture="$("$MAKE_BIN" -C "$ROOT" -s --no-print-directory \ + USB_LOOPBACK_FIXTURE=1 print-elfuse-bin)" + +if [ -z "$plain" ] || [ -z "$fixture" ]; then + echo "check-usb-fixture-bin: print-elfuse-bin produced nothing" >&2 + exit 2 +fi + +if [ "$plain" = "$fixture" ]; then + echo "Error: USB_LOOPBACK_FIXTURE=1 links to $fixture, the same path a" >&2 + echo " plain build writes, so a fixture build leaves the shipped" >&2 + echo " binary carrying the loopback model until a make clean." >&2 + echo " Point ELFUSE_BIN at \$(ELFUSE_LOOPBACK_BIN) for that flavor" >&2 + echo " (mk/config.mk), or make the choice invalidate the binary." >&2 + exit 1 +fi diff --git a/Makefile b/Makefile index 2ac3f79b..5e93165b 100644 --- a/Makefile +++ b/Makefile @@ -80,12 +80,40 @@ SRCS := \ debug/log.c \ debug/syscall-hist.c +# The USB fixture seam (src/syscall/usbdev-fixture.h). usbdev.c calls it with no +# conditional compilation of its own, so exactly one translation unit has to +# define the entry points and the choice is made here: the stub in every build, +# the loopback device model when USB_LOOPBACK_FIXTURE asks for it. Listing both +# would be a duplicate-symbol link error, which is the property that keeps a +# default build from quietly acquiring the model. +ifeq ($(USB_LOOPBACK_FIXTURE),1) +SRCS += syscall/usbdev-fixture.c +else +SRCS += syscall/usbdev-fixture-stub.c +endif + SRCS := $(addprefix src/,$(SRCS)) OBJS := $(patsubst src/%.c,$(BUILD_DIR)/%.o,$(SRCS)) +# Every host source, whether or not this build links it. Only one of the two +# fixture-seam translation units is ever in SRCS, and a static analyzer wants +# both: make lint reads this rather than SRCS so that turning the fixture off +# does not also turn off the checking of it. +ALL_SRCS := $(sort $(SRCS) src/syscall/usbdev-fixture.c \ + src/syscall/usbdev-fixture-stub.c) + DISPATCH_MANIFEST := src/syscall/dispatch.tbl DISPATCH_GENERATOR := scripts/gen-syscall-dispatch.py DISPATCH_HEADER := $(BUILD_DIR)/dispatch.h + +# The usbdevfs departed-device vectors. Generated for the same reason +# dispatch.h is: the table is data, the join against usbdev_ioctl's dispatch is +# a gate, and neither is something to hand-edit. Under build/ so that the +# formatting gates, which cover tests/*.h, have no opinion about a file a script +# writes. +DEPARTED_MANIFEST := tests/usbdev-ioctl-departed.tbl +DEPARTED_GENERATOR := scripts/gen-usbdev-ioctl-departed.py +DEPARTED_HEADER := $(BUILD_DIR)/usbdev-ioctl-departed-vectors.h HVF_LDFLAGS := -framework Hypervisor -framework IOKit -framework CoreFoundation -arch arm64 # Generated headers under build/ that must exist before compiling sources that @@ -127,6 +155,11 @@ $(DISPATCH_HEADER): $(DISPATCH_MANIFEST) $(DISPATCH_GENERATOR) src/syscall/abi.h $(BUILD_DIR)/syscall/syscall.o: $(DISPATCH_HEADER) +$(DEPARTED_HEADER): $(DEPARTED_MANIFEST) $(DEPARTED_GENERATOR) \ + src/syscall/usbdev.c | $(BUILD_DIR) + @echo " GEN $@" + $(Q)python3 $(DEPARTED_GENERATOR) --output $@ + ## Build the elfuse executable elfuse: $(ELFUSE_BIN) @@ -289,6 +322,14 @@ $(BUILD_DIR)/test-usb-desc-host: $(BUILD_DIR)/test-usb-desc-host.o \ @echo " LD $@" $(Q)$(CC) $(CFLAGS) -o $@ $^ +## Build the usbdevfs URB bookkeeping host unit test (native macOS binary) +# usbdev-urb.h is header-only arithmetic with no IOKit and no I/O, so the test +# needs no object but its own. +$(BUILD_DIR)/test-usbdev-urb-host: \ + $(BUILD_DIR)/test-usbdev-urb-host.o | $(BUILD_DIR) + @echo " LD $@" + $(Q)$(CC) $(CFLAGS) -o $@ $^ + ## Build the guest environment merge host test (native macOS binary) # guest-env.o's only dependency is the log macro, which the test stubs. $(BUILD_DIR)/test-guest-env-host: $(BUILD_DIR)/test-guest-env-host.o \ @@ -353,12 +394,27 @@ $(BUILD_DIR)/%: tests/%.c | $(BUILD_DIR) @echo " CROSS $<" $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< +# test-usbdev-ioctl-departed reads the generated vectors out of build/, so it +# needs that directory on the include path where the other guest binaries do +# not. +$(BUILD_DIR)/test-usbdev-ioctl-departed: tests/test-usbdev-ioctl-departed.c \ + $(DEPARTED_HEADER) | $(BUILD_DIR) + @echo " CROSS $<" + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -I$(BUILD_DIR) -o $@ $< + # test-usbdev-ioctl churns open/read/close on one usbdevfs node from four # threads, so a close and a sibling's open contend for the same fd number. $(BUILD_DIR)/test-usbdev-ioctl: tests/test-usbdev-ioctl.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread +# test-usbdev-urb-loopback opens a second usbdevfs node from a thread while the +# first is closing, so the two contend for one guest fd number. +$(BUILD_DIR)/test-usbdev-urb-loopback: \ + tests/test-usbdev-urb-loopback.c | $(BUILD_DIR) + @echo " CROSS $< (with -lpthread)" + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread + # test-eventfd-semaphore-contended races two blocking readers on one eventfd. $(BUILD_DIR)/test-eventfd-semaphore-contended: \ tests/test-eventfd-semaphore-contended.c | $(BUILD_DIR) diff --git a/README.md b/README.md index a9d80967..9de803ed 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ linker resolved against an external sysroot via `--sysroot`. - Synthetic `/proc` and selected `/dev` emulation for user-space probes - USB device passthrough: `/dev/bus/usb` and `/sys/bus/usb/devices` are built from the IOKit registry, and opening a device node yields a - usbdevfs fd whose synchronous ioctls (interface claim, control and bulk - transfers) drive the attached device through IOKit. Asynchronous URB - submission is not implemented; a udev-backed `lsusb` also needs - `name_to_handle_at`, and macOS publishes no root hubs, so there are no - `usbN` entries and `lsusb -t` lists devices without their bus rows + usbdevfs fd whose ioctls -- interface claim, control and bulk transfers, + and asynchronous URBs -- drive the attached device through IOKit. A + udev-backed `lsusb` also needs `name_to_handle_at`, and macOS publishes + no root hubs, so there are no `usbN` entries and `lsusb -t` lists devices + without their bus rows - Guest-internal FUSE: `/dev/fuse` and `mount("fuse")` work without macFUSE / FUSE-T / FSKit - Built-in GDB Remote Serial Protocol stub usable from `gdb` or `lldb` @@ -223,6 +223,13 @@ do. mask); the host scheduler picks the actual CPU. - `/proc`, `/dev`, and mount data are synthetic compatibility views, not host pass-throughs. +- USB interfaces bound to an Apple class driver (CDC serial, HID, FTDI) + cannot be claimed; `CLAIMINTERFACE` reports `EBUSY`, and root-mode + device capture is not implemented. CDC serial devices are reachable by + opening the host's `/dev/cu.*` node instead. +- USB mass storage will never be claimable, even with capture; macOS + does not release it. +- Isochronous URBs are unimplemented; submitting one reports `EINVAL`. - `uname` and `/proc/version` report Linux 6.18 LTS, a floor for version-gated userspace; `src/syscall/dispatch.tbl` states what is implemented. diff --git a/docs/internals.md b/docs/internals.md index 2ada3b61..4c1587d1 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -607,6 +607,7 @@ waiter enqueue, so the compare-and-wait is a single critical section. | Sysroot snapshot | `pthread_mutex` | `src/syscall/proc-state.c` | | Synthetic USB tree (scratch dirs + device model) | `usb_lock` (leaf) | `src/runtime/usb-sysfs.c` | | usbdevfs fd side table | `usbdev_table_lock` + per-entry lock | `src/syscall/usbdev.c` | +| usbdevfs event-thread startup (runloop + notify port) | `usbdev_loop_lock` (leaf) | `src/syscall/usbdev.c` | Lock ordering is documented inline in those files (`mmap_lock` is order 1, `fd_lock` is order 3, `sfd_lock` is order 5a) @@ -937,13 +938,14 @@ everything on it. `dup` of the fd is refused with `EBADF` (the side table is keyed by the guest fd and IOKit plugin handles are process-local). -The ioctl surface at this layer: `CLAIMINTERFACE` / `RELEASEINTERFACE`, -`SETINTERFACE`, `SETCONFIGURATION`, `CLEAR_HALT` / `RESETEP`, `GETDRIVER`, -`GET_CAPABILITIES`, `GET_SPEED`, `CONNECTINFO`, `DISCONNECT_CLAIM`, -`USBDEVFS_IOCTL` `DISCONNECT` / `CONNECT`, and the synchronous `CONTROL` -and `BULK` transfers, which bounce guest data through host buffers around -`DeviceRequestTO` and `ReadPipeTO` / `WritePipeTO`. The transfers here are -synchronous: the guest thread blocks in the ioctl for the duration of the +The ioctl surface at this layer is whatever `usbdev_ioctl` dispatches, and it +is written down in exactly one place a reader can check: +`tests/usbdev-ioctl-departed.tbl` carries a row per request and +`scripts/gen-usbdev-ioctl-departed.py` fails when the table and the dispatch +disagree. Two of those requests are the synchronous `CONTROL` and `BULK` +transfers, which bounce guest data through host buffers around +`DeviceRequestTO` and `ReadPipeTO` / `WritePipeTO`; they are synchronous in the +guest too, in that the guest thread blocks in the ioctl for the duration of the request. errno fidelity is the design rule, because libusb and friends branch on @@ -987,16 +989,36 @@ the fd stays nonblocking whatever the guest asks -- and always reports 0. `.fasync`. Transfer memory is one allowance for everything in flight, not a per-call -size cap: Linux charges `len + sizeof(struct urb)` against a module-global -`usbfs_memory_mb` (16 MB) and refunds it when the transfer settles, so a -request of exactly the allowance never fits and concurrent transfers across -different fds contend for the same total. An atomic counter carries it here, -charged where Linux charges it and refunded on every exit including the error -arms. A claim that IOKit answers `kIOReturnExclusiveAccess` is `EBUSY`, +size cap: Linux charges against a module-global `usbfs_memory_mb` (16 MB) and +refunds when the transfer settles, so a request of exactly the allowance never +fits and concurrent transfers across different fds contend for the same total. +What is charged depends on the path and not on the caller: `do_proc_bulk` and +`proc_do_submiturb` book `len + sizeof(struct urb)`, while `do_proc_control` +books a fixed `PAGE_SIZE + sizeof(struct urb) + +sizeof(struct usb_ctrlrequest)` whatever `wLength` says, because the kernel +bounces every control transfer through one whole page. An atomic counter +carries all three here, charged where Linux charges each and refunded on every +exit including the error arms. `CONTROL` used to be outside it, which is the +one way a guest could keep transferring after the allowance was full. +A claim that IOKit answers `kIOReturnExclusiveAccess` is `EBUSY`, what Linux reports for an interface held by a kernel driver. `GETDRIVER` names the bound Apple driver, or `usbfs` for an interface any usbfs fd on this device holds, or reports `ENODATA`; a user-client child is not a driver, -so another usbfs consumer's `USBInterfaceOpen` is not reported as one. +so another usbfs consumer's `USBInterfaceOpen` is not reported as one. It and +the three claim/connect ops reach the interface through one +`CreateInterfaceIterator`, and `SETCONFIGURATION` opens that same enumeration +to ask whether any interface has a host driver bound. The `usbfs` answer is +the one that has to be made to reach it as well: a claim is this layer's own +bookkeeping, which knows nothing about the device and stays true after it +leaves, so each of those ops opens the enumeration for the device question +before the claim answers rather than instead of it. A device-gone answer +from the call is about the device, so each reports `ENODEV` and stamps the +fd, rather than reporting the interface missing from a device that is missing +entirely or -- for `SETCONFIGURATION`, which is asking about drivers and not +about one interface -- reading an enumeration that never ran as "no driver is +bound" and changing the configuration of a device that had departed. Linux +gets the same answer one level up, from the `connected()` gate +`usbdev_do_ioctl` runs before any of them. Transfer errors map per `devio.c`: a stall is `EPIPE`, a timeout `ETIMEDOUT`, a vanished device `ENODEV`. `kIOReturnAborted` maps to `EINTR` with `syscall_restart_forbid()`, because the transfer was already on the wire @@ -1005,20 +1027,87 @@ and a dispatcher restart would send it twice. Known gaps at this stage, each printed as an XFAIL by `tests/test-usbdev-ioctl.c` rather than only written down here: -- `GET_CAPABILITIES` reports 0. Every capability bit names part of the - SUBMITURB/REAPURB machinery, which answers `ENOTTY` here. -- No disconnect gate. Linux answers `ENODEV` for every ioctl once the device - is gone; the answers served from the open-time model (`GET_SPEED`, - `CONNECTINFO`, `GET_CAPABILITIES`, `read()`) still report it. Noticing the - disconnect needs an IOKit termination notification on a run loop, which is - the async stage's machinery. +- `GET_CAPABILITIES` reports `ZERO_PACKET | REAP_AFTER_DISCONNECT`, the two + the URB engine below honors. `BULK_CONTINUATION` stays clear because the + flag is accepted without its error-cascade unlink, and the two remaining + bits describe URB splitting IOKit does not expose. - Two ioctls on one fd serialize, because the entry lock is held across the blocking transfer. Linux drops the device lock around the URB wait. - `GETDRIVER` reports the IOKit class name (`AppleUSBACMControl`) where Linux reports the driver's name (`cdc_acm`), and `DISCONNECT_CLAIM`'s name filters compare against it. -- `RESET` clears the claimed pipes' stalls and reports success instead of - re-enumerating the port, which would destroy the handles. +- `RESET` kills the device's URBs and clears the claimed pipes' stalls + instead of re-enumerating the port, which would destroy the handles. The + URB kill is the half `usb_reset_device` does have; the re-enumeration is + the half it does not get. A per-pipe stall clear that fails is logged, not + returned: the clears are the substitute, and refusing a `RESET` Linux + performs would be the worse answer. +- `CLEAR_HALT` and `RESETEP` cancel whatever is outstanding on the endpoint, + so a queued async URB there reaps `ECONNRESET`. Linux's + `check_reset_of_active_ep` only warns and leaves the queue alone, and + IOKit exposes no stall clear that does not abort the pipe. libusb calls + `libusb_clear_halt` between transfers, so this is reachable in ordinary + use. +- The per-endpoint abort shutter (`ep_aborting`) is an invariant of the paths + that abort deliberately, not of the slot: `DISCARDURB` and every wholesale + kill raise it, `CLEAR_HALT` and `RESETEP` do not, because + `ClearPipeStallBothEnds` aborts the pipe as a side effect and IOKit exposes + no variant that does not. A queued follower can therefore be started behind + a stall clear's abort that is still in flight. Linux has no such shutter to + keep; the guest-visible half of the gap is the `ECONNRESET` row above. +- A printer's `GET_DEVICE_ID` names its interface in the **high** byte of + `wIndex` and an alt setting in the low one, and `check_ctrlrecip` lets that + one request through untouched: it returns before the `index &= 0xff` when + the request type is `0xa1`, the request is `0`, and + `usb_find_alt_setting(actconfig, index >> 8, index & 0xff)` has class + `USB_CLASS_PRINTER`. Both control paths here read `wIndex & 0xff` as the + interface number for every non-vendor interface recipient, so on a printer + such a request implicitly claims the alt setting's number instead of the + interface's. Demonstrating it needs a printer attached, so it is recorded + rather than modeled. +- A URB's `signr`, like `DISCSIGNAL`'s, is accepted, reported as success and + never delivered: elfuse has no async guest-signal injection from the event + thread. +- `DISCARDURB` waits for IOKit's abort to settle, so a `REAPURBNDELAY` + issued straight afterwards finds the URB the way it does on Linux, but the + wait is bounded at 2 s rather than unbounded. The kernel call this matches + is the one `proc_unlinkurb` actually makes, `usb_kill_urb`, which is + synchronous ("upon return all completion handlers will have finished") -- + not `usb_unlink_urb`, the asynchronous unlink, which `proc_unlinkurb` does + not call. Waiting is therefore the faithful half and the 2 s ceiling is the + divergence: Linux never gives up, so `libusb_cancel_transfer` on a wedged + endpoint blocks there for as long as the transfer lives, where here it + returns 0 after 2 s with the record still flagged and still reapable when + its completion arrives. An abort that is refused outright is a different + answer and gets one: `kIOReturnNoDevice` from `AbortPipe` or + `USBDeviceAbortPipeZero` means the user client has no device behind it, so + it aborted nothing and no completion is coming. Both used to be cast to + `void`, which spent the whole 2 s waiting for a callback that could not + arrive, returned 0 to the guest with the fd unstamped, and left the URB in + the pending list where the next `REAPURB` waited on it for ever. It now + stamps the fd, the way every other op's device-gone answer does, and hands + the record back as an orphan -- reapable at once, with its buffer and + handles pinned until a callback that may never come. +- `SETINTERFACE` and `SETCONFIGURATION` report `EBUSY` when that same 2 s + drain expires with transfers still outstanding. Linux cannot reach this: + `usb_kill_urb` does not give up, so the kill always finishes and the change + always proceeds. Both retire the handles the survivors still reference -- + the interface's pipeRefs, and the whole pipe table -- so the two decide it + once, in `usbdev_drain_for_change`, rather than each on its own. A drain + that expires settles the slot's own counters, unlinks the survivors, hands + each of them back on the completed list with the `ENOENT` `usb_kill_urb` + leaves, pins the handle it still references -- the interface's, or the + device's for an ep0 record, which names no interface -- and then restarts + every endpoint FIFO exactly as the successful path does: + `draining` shut all of them, including the ones the kill did not match, and + an endpoint whose leader completed inside the window would otherwise be left + with a queued URB, none in flight and nothing to restart it. The + process-wide byte budget is not settled there -- it accounts live memory, + and a survivor's buffer is still owned by an in-flight IOKit transfer, so + its charge is given back where the buffer is freed, in the late callback. + `USBDEVFS_RESET` deliberately proceeds instead: a wedged transfer is the + state a guest issues `RESET` to escape, and a stall clear leaves the + pipeRefs where they are. - A short bulk OUT is `EIO`: `WritePipeTO` reports no length, and reporting the requested count would spell a partial write as a complete one. - The side table holds 32 open fds per process and reports `ENOMEM` past @@ -1026,6 +1115,338 @@ Known gaps at this stage, each printed as an XFAIL by - `USBDEVFS_IOCTL DISCONNECT` of an interface another usbfs fd holds is `EBUSY`; Linux releases that claim and answers 0. +### The URB Engine + +`SUBMITURB` validates the URB type and flags the way `devio.c` does, copies +the guest buffer into a host bounce buffer on the vCPU thread, and queues +the URB per endpoint. At most one URB per endpoint is in flight at IOKit at +a time; later submissions queue inside elfuse and are started from the +completion callback. The queue exists because cancellation granularity +differs: `DISCARDURB` must kill exactly one URB, but IOKit's `AbortPipe` +(`USBDeviceAbortPipeZero` for ep0) aborts every outstanding transfer on the +pipe. + +"Per endpoint" is a list and not a filter. Each pending record sits in the +fd-wide pending list and in its endpoint's own intrusive FIFO at once, and that +FIFO's head is the record in flight there when the endpoint has one and the +next to start otherwise -- so append, unlink and restart are each O(1). Reading +the same three answers out of the fd-wide list was not: every completion walked +it twice, once to unlink and once to find its endpoint's next record, and every +submit walked it a third time to decide whether the endpoint was busy, all +under `async_lock` on the one event thread that carries every fd's completions. +Measured on the loopback fixture as the median of 31 submit-to-reap round trips +on one endpoint against a second endpoint's pending depth, before: 0.017 ms at +depth 0, 0.334 at 20k, 1.848 at 80k and 2.688 at 100k. After: 0.016, 0.025, +0.020 and 0.021 ms. Linux is O(1) both ways (`list_move_tail`, +`devio.c:634`). The fd-wide list stays, because five callers genuinely need +every record and not one endpoint's: the wholesale abort, the drain's +still-busy scan, the drain timeout's orphaning, `DISCARDURB`'s lookup by user +pointer, and the reap's "is anything still out" test. The three extra link +words take the record from 136 to 160 bytes, which the process-wide byte budget +pays for: 104857 zero-length URBs now fit in the 16 MB allowance where 123361 +did. + +One URB on the wire is not by itself enough. `DISCARDURB` drops `async_lock` +to issue the abort, and in that window the target can complete normally and +the completion callback can start the queued follower, which the abort then +cancels instead -- measured against the attached board at a handful per +hundred thousand at natural rates, and reproducibly there with the window +widened, as the discarded URB reaping success and its innocent successor +reaping `ECONNRESET`. The rate is the board's: the loopback fixture +retargets an aborted transfer's timer under its own lock and cannot start a +follower into an abort that is still running, so it shows this at no rate at +all. The endpoint's FIFO therefore stays shut for the duration of the abort, +and `DISCARDURB` waits for the record to leave the pending list before it +returns. With both, a discarded URB reaps `ENOENT` and any other abort reaps +`ECONNRESET`, mirroring `usb_kill_urb` against an async unlink. + +Two caps that used to be elfuse's own are gone. The 16 MB budget is one +process-wide byte count, the way `usbfs_memory_usage` is one kernel-wide +static, and there is no URB-count cap: a 257th eight-byte URB on one fd +used to be `ENOMEM` against a 16 MB budget, which is exactly the ring depth +libusb's async API builds for bulk streaming. A URB's record is charged +alongside its buffer, so zero-length URBs are bounded too. + +Completions land on one lazily-started host thread driving a CFRunLoop -- +the first CFRunLoop in the codebase -- fed by +`CreateDeviceAsyncEventSource` / `CreateInterfaceAsyncEventSource`, which +is libusb's own darwin model. The teardown discipline is inherited from +netlink's blocking-recv rule (netlink.c): no host thread ever touches +guest memory. The +callback moves only usbdev-owned host memory (fd slots, URB records, the +completion pipe); copy-in at submit and copy-out at reap both run on the +vCPU thread. That is what makes the thread safe across `execve` and guest +teardown, so it is started once and never joined. + +`REAPURB` blocks in `io_wait_fd_or_interrupted` on the completion pipe and +copies the result out on the vCPU thread. What that pipe carries is a level, +not a running count: it holds exactly one byte while the fd has a completion +to hand back or has been disconnected, and none otherwise, restored under +`async_lock` by every path that can move either term. A byte per completion +would not survive the absence of a URB-count cap -- a zero-length URB costs +only its record against the 16 MB budget, so far more records fit than the +pipe has room for bytes, and a byte that does not fit would leave its record +reapable with nothing readable behind it. The `actual_length` it reports is +clamped to the buffer the guest submitted, the way every Linux HCD bounds +`urb->actual_length` by `transfer_buffer_length`; IOKit's transferred count is +a device-supplied number and is not passed through. A signal interrupts it +with `EINTR` and `syscall_restart_forbid()`, because Linux's reap path does +not restart. `REAPURBNDELAY` reports `EAGAIN` when nothing has completed. +`ZERO_PACKET` on a maxpacket-multiple OUT emits the trailing zero-length write +from the completion callback, and `SHORT_NOT_OK` turns a short IN into +`EREMOTEIO` at completion. + +### Poll Semantics + +usbfs readiness is inverted relative to a pipe: `POLLOUT` means "completed +URBs are reapable", and `POLLIN` is never signaled. The completion pipe's read +end raises host `POLLIN`, so `ppoll` and `pselect6` remap it to the +guest-visible `POLLOUT | POLLWRNORM`, and epoll registers `EVFILT_READ` on the +pipe but reports `EPOLLOUT`, through a per-registration flag. The host +interest is always armed, whatever events the guest asked for: a disconnected +device raises the unmaskable `POLLERR | POLLHUP`, and the pipe is the only +wake source that reaches a read-only waiter. Disconnect raises the readiness +level and nothing lowers it again, which is what a sticky `POLLERR | POLLHUP` +needs. Because the level is one token rather than a count, an `EPOLLET` +registration is edged once per rise rather than once per completion. A +consumer that reaps to `EAGAIN` sees no difference, which is the contract +`EPOLLET` states and the one `tests/test-epoll-edge.c` pins; one that reaps a +single URB per wake and re-arms would wait here where Linux, whose usbfs wakes +the wait queue once per completion, fires again. Stated rather than tested: +reaching it needs a guest that breaks the `EPOLLET` contract. A wake that maps +to nothing guest-visible re-blocks: the woken entry's host interest is +withdrawn for the rest of the call (unreaped completions keep the pipe +readable, so leaving it armed would busy-spin), the wait resumes in bounded +slices that re-check the lock-free disconnect map, and epoll additionally +re-adds a fired `EV_ONESHOT` knote disabled so a wake the guest never saw +cannot consume an `EPOLLONESHOT` arm. epoll_wait therefore never returns 0 +before its timeout, matching Linux `ep_poll`. Undoing those mutes on the way +out reads the registration and applies the knote change in one step under the +instance lock: with the lock dropped in between, a concurrent `EPOLL_CTL_MOD` +could re-arm the registration after the delete had been decided on, and the +`EV_DELETE` then retired the knote that `MOD` had just installed, leaving an +active registration with nothing behind it. + +`EPOLLPRI` is not defined in this layer and nothing below produces the +conditions Linux raises it for -- socket out-of-band data, a `sysfs` attribute +poke -- so it is left unmodeled rather than half-wired onto `EVFILT_EXCEPT`, +which would be a guess about which of the two the guest meant. A registration +naming `EPOLLPRI` alone arms no filter and so hears nothing at all, the +otherwise unmaskable `EPOLLERR | EPOLLHUP` included, since both reach the guest +only through a knote that fired. + +Both readiness masks are read as pairs, on every registration and not only on a +usbfs one. `eventpoll` has no mask of its own: `ep_item_poll` masks the file's +answer by `epi->event.events`, so a registration naming only `EPOLLRDNORM` or +only `EPOLLWRNORM` is a legal registration Linux both wakes and reports back in +the spelling it was asked in. Every readiness source below raises the bit +alongside its `EPOLLIN` or `EPOLLOUT` twin -- a pipe, a socket and a tty on the +read side, and on the write side a pipe with room, a writable TCP socket, and a +unix or datagram socket, which add `EPOLLWRBAND` as well -- so this layer arms +`EVFILT_READ` for either read bit and `EVFILT_WRITE` for either write bit, at +`ADD`, at `MOD` and at `DEL`, and reports back the intersection rather than the +canonical spelling. `tests/test-epoll.c` pins both halves. Two asymmetries are +deliberate. Linux `EPOLLWRBAND` (0x200) is in neither pair, and neither is its +`poll` twin: `POLL_WRITE_EVENTS` is `POLLOUT | POLLWRBAND`, but that token is +the *macOS* `POLLWRBAND`, which is 0x100 -- the value Linux spells +`POLLWRNORM` -- so the two halves arm on the same two Linux bits, 0x104, and +Linux `POLLWRBAND` is absent from both. And a registration naming no read bit +at all still has its read filter armed for `EPOLLRDHUP`, so that one arm +reports the `EPOLLIN` it always did rather than an entry with no events in it. +A usbfs fd is unaffected by either: its read filter is armed whatever the mask +says, and `usbdev_poll` raises neither read bit ever. + +### Disconnect And Fork + +An `IOServiceAddInterestNotification` on the runloop (or +`kIOReturnNoDevice` / `kIOReturnNotAttached` from any op -- every op +translates its IOKit status through `usbdev_ioret_op`, the synchronous +transfers and the setup calls included, so an op that sees either of those +two codes originates the disconnect rather than only reporting it) marks +**every** usbfs fd open on that device disconnected, not only the one that +noticed: poll reports `POLLERR | POLLHUP`, `REAPURB` hands back every URB and +then reports `ENODEV`, and once an fd carries that mark every other usbdevfs +ioctl on it reports `ENODEV` too -- the usbfs disconnect contract. Only +usbdevfs: the requests `do_vfs_ioctl` answers before `f_op->unlocked_ioctl` +never reach this layer's gate on Linux and do not here either, so a marked fd +still answers `FIONBIO` 0 and `FIOQSIZE` `ENOTTY`. Not meeting the gate is not +the same as matching Linux, though: this layer models none of those requests and +answers `ENOTTY` to all ten, where Linux agrees on two and answers from the +superblock, from `CAP_SYS_ADMIN` or from the argument for the other eight. +`check_vfs_ioctls` in `tests/test-usbdev-ioctl.c` carries both values for each, +drives them on a read-only and on a writable fd, and prints the eight as +`XFAIL`. The scope is the +mark, not the device: an fd that has not been told yet answers per request, +and which requests answer what is `tests/usbdev-ioctl-departed.tbl` rather +than a sentence here. `tests/test-usbdev-ioctl-departed.c` drives both halves, +the marked fd's whole usbdevfs surface and the requests beside it that are not +usbdevfs included. The cross-fd half of +it is what `usbdev_remove` does by walking `udev->filelist`, and it has to be +a walk here for the same reason: a device is gone for every consumer of it at +once, and a second fd on the node has nothing of its own that would find out. +`usbdev_ioret_device_gone`, the predicate that decides origination, is +deliberately narrower than the `ENODEV` row of `ioret_neg_errno`, which also +carries `kIOReturnNotOpen`. That code is what IOKit answers for a handle +nobody has opened yet, so a control request that arrives before the lazy +`USBDeviceOpen` draws it from a device that is plainly still attached: it +answers `ENODEV`, the way Linux answers for a device it cannot reach, and it +does not stamp. Widening the predicate to cover it would mark a live fd gone +on its first control request. + +"Every URB" is what +`CAP_REAP_AFTER_DISCONNECT` promises, stated as one invariant: after a +disconnect every URB still in flight comes back **before** any reap answers +`ENODEV`, whichever reap flavor asked first. `usbdev_remove` delivers it for +free by running `destroy_all_async` -- a synchronous `usb_kill_urb` each -- +before it wakes the reapers, so a Linux reap cannot observe the disconnect +until the pending list is already empty. IOKit completes nothing of its own +when a device terminates, so the first reap that finds the completion list +empty on a disconnected fd issues that kill itself, and its aborts land +asynchronously. The window Linux does not have -- disconnected, aborts issued, +URBs not back yet -- is real here, so `ENODEV` is decided by the pending list +being empty and never by a flag saying some earlier pass did the work. A +blocking `REAPURB` waits for the kill to settle, latch or no latch. +`REAPURBNDELAY` issues the same aborts, does not wait for them, and answers +`EAGAIN` -- `proc_reapurbnonblock`'s other arm -- until they land: a +non-blocking ioctl that sat out the two-second drain deadline holding the fd's +`async_lock` against every `SUBMITURB` and `DISCARDURB` was the worse half of +the contract, but so was answering `ENODEV` with the URBs still in flight, +which is what a one-shot flag doing both jobs did to every reap behind it. + +Both flavors reach the end of that conversation, and reach it inside one drain +deadline of the aborts. A wire can answer no abort at all, and then what +empties the pending list is the deadline: the blocking reap spends it in the +kill's wait and orphans the survivors on the way out, and the non-blocking one +measures the same deadline instead of waiting it out and orphans them itself. +Without that second half only the blocking flavor had a ceiling, so a +`libusb_handle_events_timeout(0)` loop -- non-blocking reaps and nothing else, +the common shape -- got `EAGAIN` for ever against a wedged endpoint while +`poll` held `POLLERR|POLLHUP`, where Linux answers +`connected(ps) ? -EAGAIN : -ENODEV` and the application tears down. What the +deadline gives up on is the wire answering, not the URB: the record is handed +back on the same pass that gives up on it, carrying the `ENOENT` +`destroy_all_async`'s `usb_kill_urb` leaves, and stays alive behind that +hand-back until IOKit's callback arrives -- whichever of the reap and the +callback comes second frees it. That is the last place +`CAP_REAP_AFTER_DISCONNECT` did not hold: the deadline used to unlink the +record, mark it and drop it, so the pass that gave up answered `ENODEV` with +the URB pointer never returned. + +The +refcon that carries the disconnect notification packs the slot index in a +field sized from the slot table, not in a hand-written four bits: with 32 +slots and four bits, slot 16+k decoded as slot k, so half the table never +saw a disconnect and the other half could be marked gone while attached. Across `fork` the fd is dropped and the child sees +`EBADF`, like `FD_NETLINK` and `FD_INOTIFY` today: IOKit plugin handles +are Mach-port-backed and cannot cross the `posix_spawn` that implements +fork. + +### Testing The Engine Without Hardware + +IOKit publishes no loopback device, so the async engine had no in-tree lane at +all: `ELFUSE_USB_FIXTURE`'s devices have no IOKit service behind them and stop +at `SUBMITURB`'s argument gate. `ELFUSE_USB_FIXTURE=loopback` adds one that +does, by substituting at the narrowest place that leaves every layer above it +real: the two COM vtables. Every wire call in `usbdev.c` goes through +`IOUSBDeviceInterface650 **` or `IOUSBInterfaceInterface800 **` as +`(*h)->Method(h, ...)`, so `src/syscall/usbdev-fixture.c` hands back an object +whose first member is a vtable of the same shape and nothing above it changes. +The URB records, the per-endpoint FIFO, the completion callback, `urb_status`, +the ZLP predicate, the readiness and disconnect maps, `REAPURB`, the drain and +all of `poll.c` are the same code that runs against a board. Completions arrive +from a one-shot `CFRunLoopTimer` on the event thread, which is where +`IODispatchCalloutFromCFMessage` would have delivered them. + +What the fixture does is a script rather than a flag: +`ELFUSE_USB_LOOPBACK=ep02:delay(80),ok;ep81:short(8)` and the rest of the +vocabulary in `src/syscall/usbdev-fixture.c` name the `IOReturn` each outcome +stands for, and a guest can rewrite the script, read back a log of what crossed +the seam and terminate the device through vendor control requests. The log is +what makes the `ZERO_PACKET` trailing packet observable rather than inferred. + +The seam is five `if (u->fake)` branches, one has-device probe and one bind +call in `usbdev.c`, all behind a mode resolved once per process, and the flag +is set only for the one location the fixture models -- so the other fixture modes, and every real +device, take the paths they took before. `make test-usbdev-ioctl-loopback` is +the standing check on that: the fd-contract lane must answer the same thing +with the loopback device present as without it. + +A device that can be made to leave on demand is what the third loopback lane +needs. `make test-usbdev-ioctl-departed` terminates the fixture's device once +and then drives every usbdevfs ioctl the layer implements against it, asserting +four things per request: the `ioctl(2)` return, the `errno` behind it, the poll +revents left on the fd that asked, and the revents on another fd open on the +same node, which is what says the disconnect was recorded against the device +rather than against one caller. The surface is not a list anyone maintains: +`scripts/gen-usbdev-ioctl-departed.py` reads it out of `usbdev_ioctl`'s own +dispatch and refuses to emit the lane's vectors unless every request it +dispatches has a row in `tests/usbdev-ioctl-departed.tbl` saying what Linux +answers and why -- so an ioctl added to the layer fails `make check` until its +departed-device answer is recorded. `make check-usbdev-departed` is that gate on +its own. + +The default arm is in the join too, and had to be added to it: a join built +from case labels alone covers every arm except the one catching what no label +matches, which is how that arm's `ENOTTY` on a departed device survived the +table built to find exactly that kind of answer. A row may name a `USBDEVFS_` +request `usbdev.c` defines and dispatches nowhere, the generator refuses to +emit while the arm exists with no row driving it, and three rows do. + +A row whose elfuse answer differs from Linux's carries both values and is an +XFAIL: the lane fails if the difference widens and fails if it quietly closes, +so a deliberate gap stays a recorded measurement instead of becoming a sentence +in a comment. The lane runs three times because one process can hold only one +departed device: the first correct `ENODEV` stamps every fd open on the node, +so the rows that need a claim taken before the device left cannot share a +process with the rows that take it away. + +None of that is in the shipped binary. `src/syscall/usbdev-fixture.c` is a +translation unit under `src/` that only an assertion has a use for, so the +default build links `src/syscall/usbdev-fixture-stub.c` in its place: the same +seven entry points, answering `false` and `-ENODEV`, 44 bytes of text against +the model's 11 KB. `USB_LOOPBACK_FIXTURE=1` swaps the two, and it names the +binary as well: `mk/config.mk` points `ELFUSE_BIN` at `$(ELFUSE_LOOPBACK_BIN)` +for that flavor, so the fixture build writes `build/elfuse-loopback` and a +plain `make` leaves `build/elfuse` without the model. The name is what carries +that, because nothing else does. The switch changes `SRCS` and not `CFLAGS`, +and the stale-object guard in `mk/common.mk` is keyed on `CFLAGS` alone, so +while both flavors wrote one path a fixture build left the model in +`build/elfuse` and the next `make elfuse` answered "Nothing to be done" -- +measured on the tree as it stood before this split, at 834288 bytes with `nm` +finding `_usbdev_fixture_lock`, against 815984 clean, until a `make clean`. +That sequence cannot be run again to re-measure, which is what the split is +for, so those two figures stay dated to the pre-split tree and are the only +place this series records them. What reproduces on the tree as it stands is the +pair the paths now keep apart: a clean `build/elfuse` is 815984 bytes with the +symbol 0 times, and `build/elfuse-loopback` is 834304 bytes with it once. Both +are link outputs, so they hold only for the compiler that produced them: +Homebrew `clang` 22.1.8, reached through `/opt/homebrew/opt/llvm/bin` on +`PATH`, which is the same toolchain the format gate already requires. Built +with Apple `clang` 17.0.0 from `/usr/bin` instead -- which is what `CC := +clang` in `mk/toolchain.mk` finds when that directory is not on `PATH` -- the +same two commits give 817776 and 836160, deterministic: +1792 on the default +flavor and +1856 on the loopback one, a gap per flavor rather than one +constant. So a byte count that does not match here is a different compiler +before it is a different tree, and the two symbol counts, 0 and 1, are what +hold under either. +`.ci/check-usb-fixture-bin.sh` fails if the two paths are ever equal again. +Which object defines the seam is the whole difference between the two builds: +not one branch in `usbdev.c` is conditionally compiled, so the fixture cannot +drift into code the default build never compiles, and the default build still +pays the branch that keeps the fixture off every path it must not touch. + +### Deviations From Linux + +| usbfs behavior | elfuse behavior | +|---|---| +| `RESET` re-enumerates the device | asks the device first, then kills the URBs and clears the claimed pipes' stall state and returns 0; `USBDeviceReEnumerate` would tear down every open plugin handle | +| every ioctl answers `ENODEV` once the device is gone, from `usbdev_do_ioctl`'s `connected()` gate | some answer from the open-time model or from this layer's own bookkeeping instead, on an fd that has not yet been told the device left. Which ones, and what each answers, is one row per request in `tests/usbdev-ioctl-departed.tbl`; the list is not repeated here, because it was written down wrong twice | +| isochronous URBs | `EINVAL` | +| `DISCSIGNAL` delivers a signal on disconnect | signal and context stored, never delivered | +| sync `BULK` on an interrupt endpoint works (converted to an interrupt URB) | `EINVAL`; the conversion exists only on the async URB path | +| `BULK_CONTINUATION` unlinks the rest of the cascade on error | flag accepted, no cascade unlink | +| `dup` of a usbfs fd works | `EBADF` | + ## procfs And Device Emulation `src/runtime/procemu.c` intercepts a focused set of guest-visible paths diff --git a/docs/testing.md b/docs/testing.md index c356ae0a..ba71560e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -493,6 +493,7 @@ environment hook, read once and with no effect at all when unset: | `ELFUSE_USBDEV_OPEN_FAULT=info\|blob\|pipe` | fails one step of a usbdevfs open: the model lookup or the descriptor copy with `ENOMEM`, the readiness pipe with `ENFILE` | `test-usbdev-faults` | | `ELFUSE_USBDEV_PUBLISH_DELAY_US=N` | widens the window between `fd_alloc` publishing a usbdevfs fd and the side table binding it, where a close finds no entry | `test-usbdev-faults` | | `ELFUSE_USBDEV_RETIRE_DELAY_US=N` | widens the window between the side table binding a usbdevfs fd and the open's recheck, where a close can reap the entry and a sibling open can take its slot | `test-usbdev-faults` | +| `ELFUSE_USBDEV_REAP_DELAY_US=N` | widens the window between the fd-table snapshot a `REAPURB` pass takes and the side-table entry it settles readiness on, where a close and reopen can swap the description underneath it | `test-usbdev-faults` | `ELFUSE_USB_FIXTURE` is the same shape pointing at enumeration rather than failure: it stands a deterministic synthetic USB tree up in place of whatever @@ -515,9 +516,10 @@ The lanes these drive: | `test-dir-union-alias` | every route to a second fd on one description shares one position and one union state | | `test-dir-fd-budget-union` | a union directory fd costs one host descriptor, like a plain one | | `test-fstatfs-fd-identity` | `fstatfs` answers for the descriptor it pinned, not for the fd number | -| `test-usbdev-faults` | an interface number wider than the table that indexes it, each open-time failure reported as itself, and a close inside the fd publish window leaking nothing | +| `test-usbdev-faults` | an interface number wider than the table that indexes it, each open-time failure reported as itself, a close inside the fd publish window leaking nothing, and a reap that answers for the description it snapshotted rather than the fd number | +| `test-usbdev-ioctl-departed` | every usbdevfs ioctl this layer names, on a device that has gone: the return, the `errno`, the stamp on the fd that asked and the stamp on a peer fd, against the Linux answer recorded for that request | -Two lanes carry rows that are recorded rather than asserted, and print as +Some lanes carry rows that are recorded rather than asserted, and print as `XFAIL`. An `XFAIL` row is a measured Linux value the build knowingly does not meet: it is neither a pass nor a failure, it does not turn the lane red, and the value elfuse gives today is carried beside it so that a departure from either @@ -533,6 +535,135 @@ answers with its primary alone, because the backing half belongs to a stream that has gone. Both rows are load-bearing in pairs -- neither number alone separates the answers the site could give -- so both are printed. +`test-usbdev-ioctl-departed` is the same idea with the recording moved out of +the lane and into data. Its rows are generated from +`tests/usbdev-ioctl-departed.tbl`, one per usbdevfs ioctl, and a row that +diverges from Linux carries both tuples: the lane fails if the elfuse answer +stops matching the recorded one, and fails just as loudly if it starts matching +Linux's, because an XFAIL nobody notices closing is a row that should have been +retired. The generator refuses to emit at all unless every ioctl +`usbdev_ioctl` dispatches has a row, so the recording cannot fall behind the +surface it describes. + +USB-layer coverage is split by what it needs. `test-uevent-socket` needs no +hardware and runs in the matrix like any other unit test. The two usbdevfs +lanes need no hardware either, and neither of them runs in the matrix: +`test-usbdev-ioctl` drives the fd against `ELFUSE_USB_FIXTURE`, whose devices +are modeled but have no IOKit service behind them, so every answer it asserts +is that model's and the reference kernel has nothing to adjudicate; and +`test-usbdev-urb-host` is a native macOS binary over +`src/syscall/usbdev-urb.h`, the URB bookkeeping that is decided before any +transfer -- the disconnect-watch refcon, `SUBMITURB`'s argument gate, the +transferred-count clamp, the `ZERO_PACKET` predicate and the endpoint start +gate -- and sits in `NATIVE_TESTS` (`mk/config.mk`), so it is never +cross-compiled for a guest runner at all. Both run under `make check` and +nowhere else, which is what `grep usbdev tests/test-matrix.sh` says: it is +empty. The comment above `SANITIZER_SECTIONS` in `mk/tests.mk` records the same +split from the other side. That header exists because the fixture stops at the +argument gate: the async engine's first review found five defects in code no +lane executed, and the arithmetic half of it is testable on any machine. + +`test-usbdev-urb-loopback` covers the other half. IOKit publishes no loopback +device, so the fixture becomes one: `ELFUSE_USB_FIXTURE=loopback` substitutes +for the two IOKit COM vtables and for nothing above them (see +[internals.md](internals.md#testing-the-engine-without-hardware)), which puts +submit, the per-endpoint queue, the completion callback on the event thread, +`DISCARDURB`, `REAPURB` blocking and non-blocking, poll and epoll readiness, +the `CAP_REAP_AFTER_DISCONNECT` drain and the `ZERO_PACKET` trailing packet +under assertion on any machine. The `wedge` script step adds the transfer +whose abort outlives the engine's 2 s drain deadline, which is what puts the +orphaning path and the two ioctls that refuse on it under assertion too; +nothing else in the vocabulary reaches them, because every other outstanding +transfer answers an abort at once. It is also what times the drain, from both +ends: the lane asserts that a `REAPURBNDELAY` which owes the post-disconnect +kill issues its aborts and answers without waiting for them, and that a loop of +them still reaches `ENODEV` a drain deadline later rather than spinning on +`EAGAIN` for as long as the wire withholds the abort. The elapsed time is the +whole measurement in both. A `terminate` with `wIndex` bit 1 tears the claimed +interface's pipes down and changes nothing else, so `GetPipeProperties` alone +answers `NoDevice`: that is the device vanishing between the `GetNumEndpoints` +that sizes a pipe map and the calls that fill it in, and it is the only way in +the vocabulary to reach a half-built map. Bit 2 is the same idea for the one +answer only an abort can give: `AbortPipe` and `USBDeviceAbortPipeZero` return +`NoDevice` and cancel nothing, which is what a `DISCARDURB` meets when the user +client behind its handle has gone. It is its own fact rather than a consequence +of `terminate`, because a disconnect drain issues aborts and IOKit lands those: +tying the two would leave every `never` transfer outstanding for ever and no +drain would ever finish. + +Two things the lane asserts are not guest-visible at all, so the fixture counts +them and the guest reads the counts back through a control request. One so far: +device handles the layer released while the fixture still owned a transfer on +the default control pipe. On real IOKit that is a use-after-free and here it is +not -- the fixture frees a COM wrapper nothing dereferences again -- so the +count is what stands in for it, and it is read on a later fd because the +release happens at close. + +One scenario runs in a process of its own (`test-usbdev-urb-loopback +terminate-race`): it terminates the device while an fd is closing, and every +other scenario still needs that device afterwards. What it asks is whether a +terminate delivered inside a close's two-second drain can stamp a guest fd +number that a sibling has already taken. Two fds on one node carry the other +cross-fd assertion, that a disconnect one of them provokes reaches the one that +never touched the device, and a budget filled with URBs that will not complete +carries the third, that a synchronous `CONTROL` is charged against the same +allowance a synchronous `BULK` is. `test-usbdev-ioctl-loopback` re-runs the +fd-contract lane with that device present, which is the check that the seam did +not reach a path it is not supposed to touch. + +`test-usbdev-ioctl-departed` uses the same device for the one thing no other +lane can arrange: a device that leaves while fds are open on it. It terminates +the fixture's device once and then drives every usbdevfs ioctl the layer +implements, one fresh fd per request, asserting the return, the `errno`, the +poll revents on the fd that asked and the revents on a second fd open on the +same node. Three runs, because the first correct `ENODEV` stamps every fd on +the node: the requests that need a claim taken before the device left get a +process each. What the lane may assert is not written in it -- +`scripts/gen-usbdev-ioctl-departed.py` reads the ioctl surface out of +`usbdev_ioctl`'s dispatch and joins it against +`tests/usbdev-ioctl-departed.tbl`, so an ioctl added to the layer fails `make +check` until somebody records what Linux answers for it on a departed device. +`make check-usbdev-departed` runs that join by itself. + +The loopback lanes run `build/elfuse-loopback`, which they build by re-entering +make with `USB_LOOPBACK_FIXTURE=1`. That variable is also what names the +binary: `mk/config.mk` points `ELFUSE_BIN` at `$(ELFUSE_LOOPBACK_BIN)` for the +fixture flavor, so the two flavors never write the same path and `build/elfuse` +cannot be a stale copy of the fixture build. It could before, and the flavor +stamp in `mk/common.mk` had nothing to say about it, being keyed on `CFLAGS` +while the switch changes `SRCS`: `make clean; make elfuse; make +USB_LOOPBACK_FIXTURE=1 elfuse; make elfuse` printed `Nothing to be done` and +left `nm build/elfuse` finding `_usbdev_fixture_lock` until the next `make +clean`. That was measured before the split and cannot be re-measured after it; +the dated byte counts are in +[internals.md](internals.md#testing-the-engine-without-hardware). +`.ci/check-usb-fixture-bin.sh` asks make for both paths and fails if they are +the same; `make check` runs it. The model is therefore not in `build/elfuse`, +and so not in anything shipped: see +[internals.md](internals.md#testing-the-engine-without-hardware). + +What the loopback cannot answer stays on the board, and the list is short and +worth keeping honest: real timing, NAKs, maxpacket segmentation, DMA alignment +and throughput; that IOKit really delivers completions on the runloop, and the +`IODispatchCalloutFromCFMessage` opacity that motivates the URB record's atomic +owner (a timer callout is fully visible to ThreadSanitizer, so that +justification is board-only); exclusive-access arbitration and kernel-driver +binding, so `GETDRIVER` and `DISCONNECT_CLAIM` against a real driver; a real +`SET_CONFIGURATION`, `SET_INTERFACE` pipe renumbering and port `RESET`; that a +device actually receives the zero-length packet, as opposed to elfuse emitting +it under the right predicate; and a physical unplug mid-transfer. Two of the +engine's own answers are board-only for the same reason, and breaking either +of them leaves the lane green: the `ZERO_PACKET` write's dropped `async_lock` +and its bounded timeout only matter against an endpoint that NAKs, and the +fixture answers a zero-length write immediately and ignores both timeout +arguments; and so is the bystander window `ep_aborting` shuts, because the +fixture retargets an aborted transfer's timer under its own lock and can never +start a follower into an abort that is still running. The guest probes for +those live out of tree. A hardware-dependent check that does move +in must be gated on an environment variable naming the device and must skip +with a stated reason when it is absent -- a skip is not a pass, and the +skip lists above are the model: deliberate, explained, and checked. + ## Validation Strategy By Change Type Suggested minimum validation: diff --git a/docs/usage.md b/docs/usage.md index c569e032..ce27632e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -250,6 +250,39 @@ Practical notes: time; `elfuse` creates a case-sensitive APFS sparsebundle, mounts it at `PATH`, and uses it as the sysroot for this run. +## USB Devices + +Linux USB tools see the Mac's attached devices. `/dev/bus/usb` and +`/sys/bus/usb/devices` are synthesized from the IOKit registry, and opening +a device node yields a usbfs-compatible fd driven over IOKit (see +[internals.md](internals.md), section "USB Device Passthrough"). + +What works unprivileged is what macOS itself leaves unclaimed: vendor-class +and bulk interfaces, which is the debug-probe and DFU population. `lsusb`, +libusb programs, and nusb-based tools such as the probe-rs family +enumerate, claim, and transfer against those directly, including async +URBs. + +What does not: an interface bound to an Apple class driver (CDC serial, +HID, FTDI, mass storage) cannot be claimed, and `CLAIMINTERFACE` reports +`EBUSY` -- exactly what Linux reports for an interface a kernel driver +holds. For a CDC serial device, open the host's `/dev/cu.*` node from the +guest instead; the termios layer drives the real line. + +A stock distribution `lsusb` does not run yet, and the reason is not the +USB layer. usbutils reaches libusb through libudev, whose monitor socket +wants `SO_ATTACH_FILTER`; elfuse's netlink layer answers that +`ENOPROTOOPT`, libudev's monitor never starts, and `libusb_init` gives up +with `-99`: + +```console +$ build/elfuse --sysroot ./debian-sysroot /usr/bin/lsusb +unable to initialize libusb: -99 +``` + +Programs that open the device nodes themselves, or that build libusb +without the udev backend, are unaffected. + ## Debugging With GDB Or LLDB `elfuse` includes a built-in GDB Remote Serial Protocol stub. @@ -301,10 +334,10 @@ That has a few direct implications: FSKit on the host. - USB devices attached to the Mac are reachable: `/dev/bus/usb` and `/sys/bus/usb/devices` are built from the IOKit registry, and opening a - device node gives a usbdevfs fd whose synchronous ioctls (interface claim, - control and bulk transfers) drive the device through IOKit. Asynchronous - URB submission is not implemented, and macOS arbitrates per interface: a - claim fails while a host driver holds that interface open. See + device node gives a usbdevfs fd whose ioctls -- interface claim, control + and bulk transfers, and asynchronous URBs -- drive the device through + IOKit. macOS arbitrates per interface: a claim fails while a host driver + holds that interface open. See [internals.md](internals.md), section "USB Device Passthrough", for the per-ioctl gaps. diff --git a/mk/config.mk b/mk/config.mk index b0118d5b..d1a97edc 100644 --- a/mk/config.mk +++ b/mk/config.mk @@ -3,7 +3,44 @@ ENTITLEMENTS := entitlements.plist SIGN_IDENTITY ?= - BUILD_DIR := build + +# The USB loopback fixture, off by default. +# +# src/syscall/usbdev-fixture.c models one IOKit device that echoes back what +# was written to it, so the async URB engine can be driven with no board +# attached. The synthetic USB tree in runtime/usb-sysfs.c earns its place in the +# product because it lets lsusb work on a machine with no devices; a device that +# echoes back what was written to it earns nothing outside a test, so it is not +# in the shipped binary. The default build links +# src/syscall/usbdev-fixture-stub.c instead, which answers the same seam and +# models nothing. USB_LOOPBACK_FIXTURE=1 swaps the two (see the SRCS block in +# the top-level Makefile). +# +# The loopback lanes of make check need a binary that has it, and get one under +# a name of its own so a plain make still leaves build/elfuse free of it. +# +# The name is where the two flavors are kept apart, because nothing else keeps +# them apart. The switch changes SRCS and not CFLAGS, and the flavor stamp in +# mk/common.mk is $(strip $(CFLAGS)), so it never trips on this: with both +# flavors linking to build/elfuse, "make USB_LOOPBACK_FIXTURE=1 elfuse" followed +# by "make elfuse" printed "Nothing to be done" and left the fixture in the +# binary a plain make had just been asked for -- measured before this split, nm +# still finding _usbdev_fixture_lock in build/elfuse until a make clean. The +# byte counts for that build are dated in docs/internals.md and not repeated +# here, because the sequence cannot be run again to re-measure them. What is +# guaranteed now is that the fixture build and the shipped build never write +# the same path, so the shipped one cannot be a stale copy of the other; the +# objects are still shared, which is correct, because the only translation +# unit the switch changes is the fixture seam's own and every other object +# is compiled with identical flags. .ci/check-usb-fixture-bin.sh holds the +# two paths apart. +USB_LOOPBACK_FIXTURE ?= 0 +ELFUSE_LOOPBACK_BIN := $(BUILD_DIR)/elfuse-loopback +ifeq ($(USB_LOOPBACK_FIXTURE),1) +ELFUSE_BIN := $(ELFUSE_LOOPBACK_BIN) +else ELFUSE_BIN := $(BUILD_DIR)/elfuse +endif VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "unknown") # Private pseudo-syscall number used by translated guests to invoke the @@ -32,6 +69,7 @@ NATIVE_TESTS := tests/test-multi-vcpu.c tests/test-rwx.c \ tests/test-stdio-nonblock-host.c \ tests/test-guest-env-host.c \ tests/test-usb-desc-host.c \ + tests/test-usbdev-urb-host.c \ tests/test-elf-headers-host.c \ tests/test-gdbstub-host.c SPECIAL_TEST_SRCS := tests/test-lowbase-mem.c diff --git a/mk/lint.mk b/mk/lint.mk index bb6906b8..e053b6d5 100644 --- a/mk/lint.mk +++ b/mk/lint.mk @@ -21,7 +21,7 @@ endef lint: $(BUILD_DIR)/shim_blob.h $(BUILD_DIR)/version.h $(DISPATCH_HEADER) $(call require-tool,$(CLANG_TIDY),brew install llvm -- or set CLANG_TIDY=) @echo " TIDY src/" - $(Q)$(CLANG_TIDY) $(SRCS) -- $(CFLAGS) -Isrc -I$(BUILD_DIR) + $(Q)$(CLANG_TIDY) $(ALL_SRCS) -- $(CFLAGS) -Isrc -I$(BUILD_DIR) ## Re-run Infer with the uninitialized-value checker that .inferconfig disables infer-uninit: | $(BUILD_DIR) diff --git a/mk/tests.mk b/mk/tests.mk index 724bcf03..518eb7b0 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -7,7 +7,7 @@ # src/elfuse-limits.h. ELFUSE_HOST_NOFILE_MIN ?= $(shell bash "$(CURDIR)/tests/test-config.sh" --host-nofile) -.PHONY: test-hello test-all check check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-svc-tails check-skill-refs check-proof-targets test-gdbstub test-coreutils test-busybox test-shim-futex-stats test-vcpu-watchdog \ +.PHONY: test-hello test-all check check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-usbdev-departed check-svc-tails check-skill-refs check-proof-targets test-gdbstub test-coreutils test-busybox test-shim-futex-stats test-vcpu-watchdog \ test-static-bins \ test-dynamic test-dynamic-coreutils test-glibc-dynamic \ test-glibc-coreutils test-perf \ @@ -37,13 +37,15 @@ ELFUSE_HOST_NOFILE_MIN ?= $(shell bash "$(CURDIR)/tests/test-config.sh" --host-n test-linkat-symlink-fallback test-casefold-host \ test-casefold-walk-host test-absock-names-host \ test-wakeup-pipe-host test-guest-env-host \ - test-usb-desc-host test-elf-headers-host \ + test-usb-desc-host test-usbdev-urb-host test-elf-headers-host \ test-sysroot-name-unique \ test-sysroot-name-relative \ test-nosysroot-literal-names test-sysroot-outside-names \ test-sysroot-root test-usb-sysfs test-usb-sysfs-sysroot \ test-usb-sysfs-matrix \ test-usb-sysfs-overflow test-usbdev-ioctl test-usbdev-faults \ + test-usbdev-urb-loopback test-usbdev-ioctl-loopback \ + test-usbdev-ioctl-departed \ test-dir-fd-budget-union \ test-dir-backing-drain-error test-dir-union-fd-reuse \ test-fstatfs-fd-identity \ @@ -251,7 +253,8 @@ CHECK_HOST_UNIT_BINS := $(addprefix $(BUILD_DIR)/, \ test-casefold-walk-host test-absock-names-host \ test-dynamic-array-host test-string-builder-host \ test-wakeup-pipe-host test-guest-env-host \ - test-usb-desc-host test-elf-headers-host test-gdbstub-host) + test-usb-desc-host test-usbdev-urb-host test-elf-headers-host \ + test-gdbstub-host) # Lanes shared by check and check-sanitizer, in execution order: the host # unit binaries, then the name-contract lanes cheap enough for a sanitizer @@ -272,6 +275,7 @@ $(call run-host-unit,test-wakeup-pipe-host,wakeup pipe concurrency unit test) $(call run-host-unit,test-stdio-nonblock-host,launcher stdio flags across a guest) $(call run-host-unit,test-guest-env-host,guest environment merge cross product) $(call run-host-unit,test-usb-desc-host,USB descriptor blob walk unit test) +$(call run-host-unit,test-usbdev-urb-host,usbdevfs URB bookkeeping unit test) $(call run-host-unit,test-elf-headers-host,ELF header validation unit test) $(call run-host-unit,test-gdbstub-host,buffered GDB session regression) $(call run-lane,test-usb-sysfs,synthetic USB tree contract) @@ -280,6 +284,9 @@ $(call run-lane,test-usb-sysfs-matrix,every /sys and /dev/bus entry point agains $(call run-lane,test-usb-sysfs-overflow,per-bus devnum cap under 127-device overflow) $(call run-lane,test-usbdev-ioctl,the usbdevfs fd contract without hardware) $(call run-lane,test-usbdev-faults,the usbdevfs fd's forced failures) +$(call run-lane,test-usbdev-urb-loopback,the async URB engine over an IOKit loopback) +$(call run-lane,test-usbdev-ioctl-loopback,the usbdevfs fd contract with a service behind one node) +$(call run-lane,test-usbdev-ioctl-departed,the usbdevfs ioctls this layer names on a device that has gone) $(call run-lane,test-dir-fd-budget-union,a union directory fd costs one host descriptor) $(call run-lane,test-dir-backing-drain-error,a lost union listing is reported not truncated) $(call run-lane,test-dir-union-fd-reuse,a union walk answers for the directory it pinned) @@ -301,7 +308,7 @@ check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) $(CHECK_HOST_UNIT_BINS) $(CHECK_SHARED_LANES) ## Run the unit test suite plus busybox applet validation -check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-svc-tails check-skill-refs check-proof-targets test-config test-runner \ +check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-svc-tails check-skill-refs check-proof-targets check-usbdev-departed check-usb-fixture-bin test-config test-runner \ $(CHECK_HOST_UNIT_BINS) @bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v $(CHECK_SHARED_LANES) @@ -1712,17 +1719,19 @@ test-usbdev-ioctl: $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl ELFUSE_USB_FIXTURE=1 $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl ## The usbdevfs fd's failures, one forced condition per run -# Six things this descriptor has to get right cannot be provoked from a guest on -# a healthy host: a device declaring an interface number wider than the table -# that indexes by it, the three ways an open can fail before it returns, and the +# Seven things this descriptor has to get right cannot be provoked from a guest +# on a healthy host: a device declaring an interface number wider than the table +# that indexes by it, the three ways an open can fail before it returns, the # two windows an open leaves around the moment the side table binds its guest # fd -- before the bind, where a close finds no entry, and after it, where a -# close can reap the entry and a sibling open can take the slot back. Each run -# below forces exactly one and the binary asserts only that one, so a failure -# names the condition. The malformed-descriptor run is also where the -# out-of-bounds read lives: it is invisible in the answer -- both sides report -# EINVAL, which is what checkintf reports -- and shows up only under -# -fsanitize=array-bounds, which is why this lane is in the sanitizer set. +# close can reap the entry and a sibling open can take the slot back -- and the +# window a reap leaves between the fd-table snapshot it settles readiness from +# and the side-table entry it settles it on. Each run below forces exactly one +# and the binary asserts only that one, so a failure names the condition. The +# malformed-descriptor run is also where the out-of-bounds read lives: it is +# invisible in the answer -- both sides report EINVAL, which is what checkintf +# reports -- and shows up only under -fsanitize=array-bounds, which is why this +# lane is in the sanitizer set. test-usbdev-faults: $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl ELFUSE_USB_FIXTURE=badifnum $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl ELFUSE_USB_FIXTURE=1 ELFUSE_USBDEV_OPEN_FAULT=info \ @@ -1735,6 +1744,103 @@ test-usbdev-faults: $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl ELFUSE_USB_FIXTURE=1 ELFUSE_USBDEV_RETIRE_DELAY_US=20000 \ $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl + ELFUSE_USB_FIXTURE=1 ELFUSE_USBDEV_REAP_DELAY_US=20000 \ + $(ELFUSE_BIN) $(TEST_DIR)/test-usbdev-ioctl + +## Print the binary path this flavor links, for .ci/check-usb-fixture-bin.sh +# +# A goal of its own rather than a generic print-%: it is the only variable any +# caller asks for, and mk/common.mk already filters print-% out of the flavor +# guard so that asking costs no rebuild. +.PHONY: print-elfuse-bin +print-elfuse-bin: + @printf '%s\n' '$(ELFUSE_BIN)' + +## Verify the fixture build and the shipped build cannot share a binary path +.PHONY: check-usb-fixture-bin +check-usb-fixture-bin: + @bash .ci/check-usb-fixture-bin.sh + +## Verify the recorded departed-device answers still match the ioctl surface +# +# The join is the gate: the generator refuses to emit unless every request +# usbdev_ioctl dispatches has a row in tests/usbdev-ioctl-departed.tbl saying +# what Linux answers for it on a device that has gone. +check-usbdev-departed: $(DEPARTED_HEADER) + @python3 $(DEPARTED_GENERATOR) --check --output $(DEPARTED_HEADER) + +## Build the fixture-enabled binary the two loopback lanes run +# +# The loopback fixture is not in the default build (USB_LOOPBACK_FIXTURE in +# mk/config.mk), so a lane that needs it has to ask for a binary that has it. +# Asking is a recursive make with the variable set rather than a second link +# line here, so what the lanes run is the build a reader gets from +# "make USB_LOOPBACK_FIXTURE=1" and cannot drift from it, and so nothing in this +# file has to restate the prerequisites of $(ELFUSE_BIN). +# +# Into a binary of its own, so a plain make still leaves build/elfuse without +# the fixture: the sub-make overrides ELFUSE_BIN rather than BUILD_DIR, which +# keeps every object but the fixture's shared with the outer build. Overriding +# BUILD_DIR instead would recompile the tree. +# +# Phony because the sub-make is what decides whether anything needs rebuilding. +# The leading '+' is what keeps it expanding under -n: make looks for a literal +# $(MAKE) in the unexpanded recipe line (make manual 9.3). A command-line +# override reaches a sub-make through MAKEFLAGS, so the sanitizer lanes get a +# loopback binary of their own flavor without this line naming EXTRA_CFLAGS. +.PHONY: elfuse-loopback +elfuse-loopback: + +$(Q)$(MAKE) --no-print-directory USB_LOOPBACK_FIXTURE=1 \ + ELFUSE_BIN=$(ELFUSE_LOOPBACK_BIN) elfuse + +## The async URB engine, against a device that can complete a transfer +# ELFUSE_USB_FIXTURE=loopback adds one device whose IOKit answers come from +# src/syscall/usbdev-fixture.c: the two COM vtables are replaced and nothing +# above them is, so submit, the per-endpoint queue, the completion callback on +# the event thread, the readiness and disconnect maps, REAPURB, the +# CAP_REAP_AFTER_DISCONNECT drain and the ZERO_PACKET write all run here for the +# first time without a board. What it cannot cover -- real timing, NAKs, +# maxpacket segmentation, exclusive-access arbitration, a physical unplug -- is +# listed in docs/testing.md and stays on the board. +# +# Two runs, because one of the scenarios terminates the modeled device while +# another fd is closing and every scenario in the main run still needs it +# afterwards. Same shape as test-usbdev-ioctl-departed's three. +test-usbdev-urb-loopback: elfuse-loopback $(TEST_DIR)/test-usbdev-urb-loopback + ELFUSE_USB_FIXTURE=loopback \ + $(ELFUSE_LOOPBACK_BIN) $(TEST_DIR)/test-usbdev-urb-loopback + ELFUSE_USB_FIXTURE=loopback \ + $(ELFUSE_LOOPBACK_BIN) $(TEST_DIR)/test-usbdev-urb-loopback \ + terminate-race + +## The same fd contract, with an IOKit service behind one node +# The seam is per device: the loopback model adds a device and leaves the +# service-less ones alone, so this run must answer exactly what +# test-usbdev-ioctl answers. It is the check that the seam did not leak into the +# paths it is not supposed to touch. +test-usbdev-ioctl-loopback: elfuse-loopback $(TEST_DIR)/test-usbdev-ioctl + ELFUSE_USB_FIXTURE=loopback \ + $(ELFUSE_LOOPBACK_BIN) $(TEST_DIR)/test-usbdev-ioctl + +## Every usbdevfs ioctl the layer implements, on a device that has gone +# +# The surface comes from usbdev_ioctl's own dispatch by way of +# scripts/gen-usbdev-ioctl-departed.py, so a new ioctl joins this lane without +# anyone remembering to add it. Three runs because one process can hold only one +# departed device: the first -ENODEV stamps every fd open on the node, so the +# rows that need a claim taken before the device left cannot share a process +# with the rows that take it away. +.PHONY: test-usbdev-ioctl-departed +test-usbdev-ioctl-departed: elfuse-loopback check-usbdev-departed \ + $(TEST_DIR)/test-usbdev-ioctl-departed + ELFUSE_USB_FIXTURE=loopback \ + $(ELFUSE_LOOPBACK_BIN) $(TEST_DIR)/test-usbdev-ioctl-departed fresh + ELFUSE_USB_FIXTURE=loopback \ + $(ELFUSE_LOOPBACK_BIN) $(TEST_DIR)/test-usbdev-ioctl-departed \ + held-claim + ELFUSE_USB_FIXTURE=loopback \ + $(ELFUSE_LOOPBACK_BIN) $(TEST_DIR)/test-usbdev-ioctl-departed \ + held-release ## fstatfs answers for the descriptor it pinned, not for the fd number # The identity is decided from the slot's stamp and from the descriptor itself, @@ -1883,6 +1989,10 @@ test-absock-names-host: $(BUILD_DIR)/test-absock-names-host test-usb-desc-host: $(BUILD_DIR)/test-usb-desc-host $(BUILD_DIR)/test-usb-desc-host +## Run the usbdevfs URB bookkeeping unit test (native host binary) +test-usbdev-urb-host: $(BUILD_DIR)/test-usbdev-urb-host + $(BUILD_DIR)/test-usbdev-urb-host + ## Run the ELF header validation host unit test test-elf-headers-host: $(BUILD_DIR)/test-elf-headers-host $(BUILD_DIR)/test-elf-headers-host diff --git a/scripts/check-eintr-contract.py b/scripts/check-eintr-contract.py index fd48c4e7..a68f33c8 100644 --- a/scripts/check-eintr-contract.py +++ b/scripts/check-eintr-contract.py @@ -115,7 +115,16 @@ "syscall/usbdev.c::ioret_neg_errno": ( "forbids", "kIOReturnAborted means a sync transfer already handed to IOKit was " - "aborted mid-flight; a restart would send the request twice.", + "aborted mid-flight; a restart would send the request twice. The " + "event thread also maps statuses through here, where the " + "thread-local flag is dead state.", + ), + "syscall/usbdev.c::usbdev_do_reap": ( + "forbids", + "REAPURB blocks on the completion pipe for URBs an earlier SUBMITURB " + "already handed to IOKit; proc_reapurb returns -EINTR with no restart " + "(devio.c:2116-2117), so the dispatcher must not re-enter the wait " + "as if the guest had asked again.", ), # Waits that report EINTR before doing anything the guest can observe. "syscall/io.c::io_retry_backoff": ( diff --git a/scripts/gen-usbdev-ioctl-departed.py b/scripts/gen-usbdev-ioctl-departed.py new file mode 100755 index 00000000..18e44328 --- /dev/null +++ b/scripts/gen-usbdev-ioctl-departed.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +"""Generate build/usbdev-ioctl-departed-vectors.h from the recorded table. + +The gate the header exists for: the usbdevfs ioctl surface is read out of +src/syscall/usbdev.c's dispatch rather than listed by hand, and every request +it dispatches must have a row in tests/usbdev-ioctl-departed.tbl saying what +Linux answers for it on a departed device. An ioctl added to the layer with no +row fails here, so no prose has to carry the enumeration. + +The arm that catches everything the dispatch does not is part of the surface +and was not part of the join, which is how a comment claiming the table said +what every arm answers stayed true of every arm but that one. A row may +therefore name a USBDEVFS_ request src/syscall/usbdev.c defines and dispatches +nowhere; it drives the default arm. While usbdev_ioctl has a default arm, at +least one such row must exist, and a row naming a request the file does not +define at all still fails here. +""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +DEFAULT_SOURCE = ROOT / "src" / "syscall" / "usbdev.c" +DEFAULT_TABLE = ROOT / "tests" / "usbdev-ioctl-departed.tbl" +DEFAULT_OUTPUT = ROOT / "build" / "usbdev-ioctl-departed-vectors.h" + +DEFINE_RE = re.compile(r"^#define\s+USBDEVFS_([A-Z0-9_]+)\s+(0x[0-9a-fA-F]+)u\s*$") +CASE_RE = re.compile(r"^\s*case USBDEVFS_([A-Z0-9_]+):") +EARLY_RE = re.compile(r"request == USBDEVFS_([A-Z0-9_]+)") +DEFAULT_RE = re.compile(r"^\s{4}default:\s*$") + +PHASES = {"fresh": "USBDEV_DEPARTED_FRESH", + "held-claim": "USBDEV_DEPARTED_HELD_CLAIM", + "held-release": "USBDEV_DEPARTED_HELD_RELEASE"} +REVENTS = {"ERRHUP": "(POLLERR | POLLHUP)", "NONE": "0"} + + +def dispatched(path: pathlib.Path) -> tuple[dict[str, str], list[str], bool]: + """The request codes the layer defines, the ones usbdev_ioctl reaches by a + case label of its own, and whether it has a default arm to catch the rest. + """ + text = path.read_text(encoding="utf-8") + codes = {m.group(1): m.group(2) + for m in (DEFINE_RE.match(line) for line in text.splitlines()) if m} + start = text.find("\nint64_t usbdev_ioctl(") + if start < 0: + raise ValueError(f"{path}: no usbdev_ioctl definition") + body = text[start:] + end = body.find("\n}\n") + if end < 0: + raise ValueError(f"{path}: usbdev_ioctl has no closing brace") + body = body[:end] + + names: list[str] = [] + for name in EARLY_RE.findall(body) + [ + m.group(1) for m in (CASE_RE.match(l) for l in body.splitlines()) if m]: + if name in ("IOCTL_DISCONNECT", "IOCTL_CONNECT"): + continue # sub-codes of USBDEVFS_IOCTL, not requests of their own + if name not in codes: + raise ValueError(f"{path}: usbdev_ioctl dispatches USBDEVFS_{name} " + "with no request code defined for it") + if name not in names: + names.append(name) + if not names: + raise ValueError(f"{path}: usbdev_ioctl dispatches nothing") + has_default = any(DEFAULT_RE.match(line) for line in body.splitlines()) + return codes, names, has_default + + +def tuple_fields(spec: str, where: str) -> tuple[str, str, str, str]: + parts = spec.split("/") + if len(parts) != 4: + raise ValueError(f"{where}: '{spec}' is not rc/errno/stamp/peer") + rc, err, stamp, peer = parts + try: + int(rc, 0) + except ValueError: + raise ValueError(f"{where}: rc '{rc}' is not a number") from None + if err != "NONE" and not re.fullmatch(r"E[A-Z0-9]+", err): + raise ValueError(f"{where}: errno '{err}' is neither NONE nor an E name") + for name in (stamp, peer): + if name not in REVENTS: + raise ValueError(f"{where}: revents '{name}' is not one of " + + ", ".join(sorted(REVENTS))) + return rc, "0" if err == "NONE" else err, REVENTS[stamp], REVENTS[peer] + + +def parse_table(path: pathlib.Path) -> list[dict[str, object]]: + rows: list[dict[str, object]] = [] + for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + where = f"{path}:{lineno}" + if not raw.strip() or raw.lstrip().startswith("#"): + continue + if raw[0].isspace(): + if not rows: + raise ValueError(f"{where}: note with no row above it") + note = rows[-1]["note"] + rows[-1]["note"] = (note + " " + raw.strip()) if note else raw.strip() + continue + fields = raw.split() + if len(fields) != 6: + raise ValueError(f"{where}: expected 6 columns, got {len(fields)}") + rid, req, phase, lin, here, cite = fields + if phase not in PHASES: + raise ValueError(f"{where}: phase '{phase}' is not one of " + + ", ".join(sorted(PHASES))) + if any(r["id"] == rid for r in rows): + raise ValueError(f"{where}: duplicate row id '{rid}'") + rows.append({ + "id": rid, "req": req, "phase": phase, + "kernel": tuple_fields(lin, where), + "here": None if here == "-" else tuple_fields(here, where), + "cite": int(cite), "note": "", + }) + for row in rows: + if not row["note"]: + raise ValueError(f"{path}: row '{row['id']}' has no note") + return rows + + +def cross_check(rows: list[dict[str, object]], codes: dict[str, str], + names: list[str], has_default: bool, + table: pathlib.Path, source: pathlib.Path) -> None: + covered = {row["req"] for row in rows} + missing = [n for n in names if n not in covered] + if missing: + raise ValueError( + f"{table}: no recorded Linux answer for USBDEVFS_" + + ", USBDEVFS_".join(missing) + + f" (dispatched by {source}). Add a row saying what Linux answers " + "for it on a departed device.") + unknown = sorted(covered - set(codes)) + if unknown: + raise ValueError( + f"{table}: rows name USBDEVFS_" + ", USBDEVFS_".join(unknown) + + f", which {source} defines no request code for.") + fallthrough = sorted(covered - set(names)) + if fallthrough and not has_default: + raise ValueError( + f"{table}: rows name USBDEVFS_" + ", USBDEVFS_".join(fallthrough) + + f", which {source} neither dispatches nor catches: usbdev_ioctl " + "has no default arm for them to reach.") + if has_default and not fallthrough: + raise ValueError( + f"{table}: usbdev_ioctl has a default arm and no row drives it. " + "Add a row naming a USBDEVFS_ request the layer defines and does " + "not dispatch, so what that arm answers on a departed device is " + "measured rather than assumed.") + + +def c_string(text: str) -> str: + return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +def render(rows: list[dict[str, object]], codes: dict[str, str]) -> str: + out = [ + "/*", + " * What the usbdevfs ioctls this layer names answer on a departed device", + " *", + " * Copyright 2026 elfuse contributors", + " * SPDX-License-Identifier: Apache-2.0", + " *", + " * GENERATED by scripts/gen-usbdev-ioctl-departed.py from", + " * tests/usbdev-ioctl-departed.tbl; do not edit. The table records what", + " * Linux answers and why, the generator checks it against the dispatch in", + " * src/syscall/usbdev.c, and tests/test-usbdev-ioctl-departed.c drives it.", + " *", + " * Include , and before this header.", + " */", + "", + "#pragma once", + "", + "enum {", + " USBDEV_DEPARTED_FRESH,", + " USBDEV_DEPARTED_HELD_CLAIM,", + " USBDEV_DEPARTED_HELD_RELEASE,", + "};", + "", + "/* One observation of a departed-device ioctl. rc is the ioctl(2) return,", + " * err the errno behind an rc of -1, stamp the poll revents left on the fd", + " * that asked, and peer the revents on another fd open on the same node.", + " */", + "typedef struct {", + " long rc;", + " int err;", + " short stamp;", + " short peer;", + "} usbdev_departed_tuple_t;", + "", + "typedef struct {", + " const char *id;", + " const char *req_name;", + " unsigned long request;", + " int phase;", + " usbdev_departed_tuple_t kernel;", + " bool diverges;", + " usbdev_departed_tuple_t here;", + " int devio_line;", + " const char *note;", + "} usbdev_departed_row_t;", + "", + "/* One driver per row, declared here and defined by the lane, so a row", + " * with no driver behind it fails to compile rather than going undriven.", + " */", + "typedef long usbdev_departed_fn(int fd, unsigned long request);", + "", + ] + out += [f"static usbdev_departed_fn departed_drive_{row['id']};" + for row in rows] + out.append("") + out.append("static usbdev_departed_fn *const usbdev_departed_drivers[] = {") + out += [f" departed_drive_{row['id']}," for row in rows] + out.append("};") + out.append("") + out.append(f"#define USBDEV_DEPARTED_NROWS {len(rows)}") + out.append("") + out.append("static const usbdev_departed_row_t usbdev_departed_rows[] = {") + for row in rows: + lin = row["kernel"] + here = row["here"] or ("0", "0", "0", "0") + out += [ + " {", + f" .id = {c_string(str(row['id']))},", + f" .req_name = {c_string('USBDEVFS_' + str(row['req']))},", + f" .request = {codes[str(row['req'])]}ul,", + f" .phase = {PHASES[str(row['phase'])]},", + f" .kernel = {{{lin[0]}, {lin[1]}, {lin[2]}, {lin[3]}}},", + f" .diverges = {'true' if row['here'] else 'false'},", + f" .here = {{{here[0]}, {here[1]}, {here[2]}, {here[3]}}},", + f" .devio_line = {row['cite']},", + f" .note = {c_string(str(row['note']))},", + " },", + ] + out.append("};") + out.append("") + return "\n".join(out) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source", type=pathlib.Path, default=DEFAULT_SOURCE) + parser.add_argument("--table", type=pathlib.Path, default=DEFAULT_TABLE) + parser.add_argument("--output", type=pathlib.Path, default=DEFAULT_OUTPUT) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + + codes, names, has_default = dispatched(args.source) + rows = parse_table(args.table) + cross_check(rows, codes, names, has_default, args.table, args.source) + output = render(rows, codes) + + if args.check: + if not args.output.exists(): + print(f"{args.output}: missing; run " + "'python3 scripts/gen-usbdev-ioctl-departed.py'", file=sys.stderr) + return 1 + if args.output.read_text(encoding="utf-8") != output: + print(f"{args.output}: stale; run " + "'python3 scripts/gen-usbdev-ioctl-departed.py'", file=sys.stderr) + return 1 + return 0 + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except ValueError as exc: + print(exc, file=sys.stderr) + raise SystemExit(1) diff --git a/src/runtime/usb-fixture.h b/src/runtime/usb-fixture.h new file mode 100644 index 00000000..6de509f2 --- /dev/null +++ b/src/runtime/usb-fixture.h @@ -0,0 +1,52 @@ +/* + * The loopback fixture device's model, shared by the two halves that must agree + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * ELFUSE_USB_FIXTURE=loopback stands up one extra device whose descriptors come + * from usb-sysfs.c and whose IOKit answers come from syscall/usbdev-fixture.c. + * The endpoint table below is the single place both read: a descriptor blob + * that advertises an endpoint GetPipeProperties does not report (or the other + * way round) is a fixture that tests the wrong device, and the two halves live + * in different directories, so the table is a header rather than a duplicated + * literal. + * + * The addresses are the ones the out-of-tree board driver uses against the + * ESP32-S3 at 303a:1001 (interface 2, bulk OUT 0x02, bulk IN 0x81, interrupt IN + * 0x83), so a scenario can be written once and run in both places. + */ + +#pragma once + +#include + +typedef struct { + uint8_t addr; /* bEndpointAddress */ + uint8_t attr; /* bmAttributes: 0x02 bulk, 0x03 interrupt */ + uint16_t mps; /* wMaxPacketSize */ + uint8_t interval; /* bInterval */ +} usb_fixture_ep_t; + +#define USB_FIXTURE_LOOPBACK_BUS 3 +#define USB_FIXTURE_LOOPBACK_PORT 1 +#define USB_FIXTURE_LOOPBACK_DEVNUM 1 +#define USB_FIXTURE_LOOPBACK_IFNUM 2 +#define USB_FIXTURE_LOOPBACK_VID 0x303au +#define USB_FIXTURE_LOOPBACK_PID 0x1001u + +/* model_build's own arithmetic, spelled once so the IOKit half can match a + * device without re-deriving it: bus 1 is locationID 0x00xxxxxx. + */ +#define USB_FIXTURE_LOOPBACK_LOCATION \ + ((((uint32_t) USB_FIXTURE_LOOPBACK_BUS - 1u) << 24) | \ + ((uint32_t) USB_FIXTURE_LOOPBACK_PORT << 4)) + +#define USB_FIXTURE_LOOPBACK_NEPS 3 + +static const usb_fixture_ep_t + usb_fixture_loopback_eps[USB_FIXTURE_LOOPBACK_NEPS] = { + {0x02, 0x02, 64, 0}, /* bulk OUT */ + {0x81, 0x02, 64, 0}, /* bulk IN */ + {0x83, 0x03, 8, 1}, /* interrupt IN */ +}; diff --git a/src/runtime/usb-sysfs.c b/src/runtime/usb-sysfs.c index a811e3be..3f0e240d 100644 --- a/src/runtime/usb-sysfs.c +++ b/src/runtime/usb-sysfs.c @@ -53,6 +53,7 @@ #include "runtime/procemu-internal.h" #include "runtime/procemu.h" #include "runtime/usb-desc.h" +#include "runtime/usb-fixture.h" #include "runtime/usb-sysfs.h" #include "syscall/internal.h" #include "syscall/linux-wire.h" @@ -307,16 +308,27 @@ typedef struct { * what happens when a device does. */ unsigned ifnum_base; + + /* The endpoints every interface of this device carries, or NULL for the + * default of one bulk IN per interface (0x81, 0x82, ...). The loopback + * device needs a real OUT and a real interrupt IN, and the IOKit half has + * to report the same set, so the table is shared rather than written twice + * (runtime/usb-fixture.h). + */ + const usb_fixture_ep_t *eps; + unsigned neps; } usb_fixture_spec_t; /* The number of interface descriptors is the only thing that varies the blob * length: an 18-byte device descriptor, one configuration header, then one * interface descriptor per interface. */ -static size_t usb_fixture_blob_len(unsigned nifaces) +static size_t usb_fixture_blob_len(const usb_fixture_spec_t *s) { + unsigned neps = s->neps ? s->neps : 1; return USB_DEVICE_DESC_LEN + USB_CONFIG_DESC_LEN + - (size_t) nifaces * (USB_INTERFACE_DESC_LEN + USB_ENDPOINT_DESC_LEN); + (size_t) s->nifaces * + (USB_INTERFACE_DESC_LEN + (size_t) neps * USB_ENDPOINT_DESC_LEN); } /* Fill @d from @s, writing the device, configuration and interface descriptors @@ -346,9 +358,10 @@ static void usb_fixture_fill(usb_dev_t *d, const usb_fixture_spec_t *s) snprintf(d->devpath, sizeof(d->devpath), "%d", s->port); snprintf(d->name, sizeof(d->name), "%d-%d", s->busnum, s->port); - size_t cfg_total = - USB_CONFIG_DESC_LEN + - (size_t) s->nifaces * (USB_INTERFACE_DESC_LEN + USB_ENDPOINT_DESC_LEN); + unsigned neps = s->neps ? s->neps : 1; + size_t if_total = + USB_INTERFACE_DESC_LEN + (size_t) neps * USB_ENDPOINT_DESC_LEN; + size_t cfg_total = USB_CONFIG_DESC_LEN + (size_t) s->nifaces * if_total; build_device_descriptor(d, blob); uint8_t *c = blob + USB_DEVICE_DESC_LEN; c[0] = USB_CONFIG_DESC_LEN; @@ -361,15 +374,13 @@ static void usb_fixture_fill(usb_dev_t *d, const usb_fixture_spec_t *s) c[7] = 0x80; /* bmAttributes: bus powered */ c[8] = 50; /* bMaxPower: 100 mA */ for (unsigned i = 0; i < s->nifaces; i++) { - uint8_t *q = - c + USB_CONFIG_DESC_LEN + - (size_t) i * (USB_INTERFACE_DESC_LEN + USB_ENDPOINT_DESC_LEN); + uint8_t *q = c + USB_CONFIG_DESC_LEN + (size_t) i * if_total; q[0] = USB_INTERFACE_DESC_LEN; q[1] = USB_DT_INTERFACE; unsigned ifnum = s->ifnum_base + i; q[2] = (uint8_t) ifnum; /* bInterfaceNumber */ q[3] = 0; /* bAlternateSetting */ - q[4] = 1; /* bNumEndpoints */ + q[4] = (uint8_t) neps; /* bNumEndpoints */ q[5] = 0xff; /* bInterfaceClass: vendor-specific */ q[6] = 0x00; q[7] = 0x00; @@ -382,14 +393,18 @@ static void usb_fixture_fill(usb_dev_t *d, const usb_fixture_spec_t *s) * reach the code that decides between "no such endpoint" and "bad * argument". Bulk IN, one per interface: 0x81, 0x82, ... */ - uint8_t *e = q + USB_INTERFACE_DESC_LEN; - e[0] = USB_ENDPOINT_DESC_LEN; - e[1] = USB_DT_ENDPOINT; - e[2] = (uint8_t) (0x81 + i); /* bEndpointAddress: bulk IN */ - e[3] = 0x02; /* bmAttributes: bulk */ - e[4] = 0x40; /* wMaxPacketSize: 64 */ - e[5] = 0x00; - e[6] = 0; /* bInterval */ + for (unsigned k = 0; k < neps; k++) { + uint8_t *e = + q + USB_INTERFACE_DESC_LEN + (size_t) k * USB_ENDPOINT_DESC_LEN; + e[0] = USB_ENDPOINT_DESC_LEN; + e[1] = USB_DT_ENDPOINT; + e[2] = s->eps ? s->eps[k].addr : (uint8_t) (0x81 + i); + e[3] = s->eps ? s->eps[k].attr : 0x02; + uint16_t mps = s->eps ? s->eps[k].mps : 0x40; + e[4] = (uint8_t) (mps & 0xff); + e[5] = (uint8_t) (mps >> 8); + e[6] = s->eps ? s->eps[k].interval : 0; + } } } @@ -415,6 +430,15 @@ static void usb_fixture_fill(usb_dev_t *d, const usb_fixture_spec_t *s) * short of a fixture reaches the paths that index by that number. Added as a * separate mode rather than to the default set so the lanes that walk the tree * keep the device list they were written against. + * + * ELFUSE_USB_FIXTURE=loopback: the default set plus /dev/bus/usb/003/001, the + * one device with an IOKit answer behind it (syscall/usbdev-fixture.c). It + * carries interface 2 with bulk OUT 0x02, bulk IN 0x81 and interrupt IN 0x83, + * the endpoints the out-of-tree board driver uses, and the addresses come from + * runtime/usb-fixture.h so the descriptor blob here and GetPipeProperties there + * cannot drift. The default devices stay in the set, and stay service-less, so + * the fd-contract lane answers the same thing in this mode as in the default + * one. */ static int usb_fixture_specs(usb_fixture_spec_t *specs, int cap) { @@ -422,18 +446,29 @@ static int usb_fixture_specs(usb_fixture_spec_t *specs, int cap) int n = 0; if (mode && !strcmp(mode, "overflow")) { for (int port = 1; port <= 129 && n < cap; port++) - specs[n++] = - (usb_fixture_spec_t) {1, port, 0, 0x1d6b, 0x0002, 1, 0}; + specs[n++] = (usb_fixture_spec_t) {1, port, 0, 0x1d6b, 0x0002, + 1, 0, NULL, 0}; if (n < cap) - specs[n++] = (usb_fixture_spec_t) {2, 1, 0, 0x2109, 0x0100, 1, 0}; + specs[n++] = + (usb_fixture_spec_t) {2, 1, 0, 0x2109, 0x0100, 1, 0, NULL, 0}; return n; } if (n < cap) - specs[n++] = (usb_fixture_spec_t) {1, 1, 1, 0x1d6b, 0x0002, 2, 0}; + specs[n++] = + (usb_fixture_spec_t) {1, 1, 1, 0x1d6b, 0x0002, 2, 0, NULL, 0}; if (n < cap) - specs[n++] = (usb_fixture_spec_t) {2, 1, 1, 0x2109, 0x0100, 1, 0}; + specs[n++] = + (usb_fixture_spec_t) {2, 1, 1, 0x2109, 0x0100, 1, 0, NULL, 0}; if (mode && !strcmp(mode, "badifnum") && n < cap) - specs[n++] = (usb_fixture_spec_t) {1, 2, 2, 0x1d6b, 0x0002, 1, 200}; + specs[n++] = + (usb_fixture_spec_t) {1, 2, 2, 0x1d6b, 0x0002, 1, 200, NULL, 0}; + if (mode && !strcmp(mode, "loopback") && n < cap) + specs[n++] = (usb_fixture_spec_t) { + USB_FIXTURE_LOOPBACK_BUS, USB_FIXTURE_LOOPBACK_PORT, + USB_FIXTURE_LOOPBACK_DEVNUM, USB_FIXTURE_LOOPBACK_VID, + USB_FIXTURE_LOOPBACK_PID, 1, + USB_FIXTURE_LOOPBACK_IFNUM, usb_fixture_loopback_eps, + USB_FIXTURE_LOOPBACK_NEPS}; return n; } @@ -473,10 +508,10 @@ static void model_build(void) * canned-model allocation stashed in a callee would read to the * leak analyzer as unowned once it escaped into usb_devs[]. */ - d->blob = malloc(usb_fixture_blob_len(specs[k].nifaces)); + d->blob = malloc(usb_fixture_blob_len(&specs[k])); if (!d->blob) continue; - d->blob_len = usb_fixture_blob_len(specs[k].nifaces); + d->blob_len = usb_fixture_blob_len(&specs[k]); usb_fixture_fill(d, &specs[k]); usb_ndevs++; } diff --git a/src/syscall/internal.h b/src/syscall/internal.h index 0f9a1c6e..bf0aac76 100644 --- a/src/syscall/internal.h +++ b/src/syscall/internal.h @@ -124,10 +124,17 @@ * it. What keeps the slot from being torn * down and reused in between is the refs * and dead pair the lookup sets under the - * table lock, not a nesting. Never held - * together with any other file-scope lock - * in this list either, in either direction, - * so its position here is nominal + * table lock, not a nesting. The per-entry + * lock does hold the per-entry async_lock + * beneath it (usbdev_teardown_locked), and + * so does the table lock, one slot's at a + * time and never two at once: the cross-fd + * disconnect walk needs a consistent view + * of which slots are live while it stamps + * them. Never held together with any other + * file-scope lock in this list either, in + * either direction, so its position here is + * nominal * * Leaves. Each of these is the innermost lock on every path that takes it, so * it has no position in the order above and cannot be half of an inversion: @@ -141,6 +148,8 @@ * sysinfo_lock (sys.c) * sysroot_lock (proc-state.c) * usb_lock (runtime/usb-sysfs.c) + * usbdev_loop_lock (usbdev.c) + * usbdev_fixture_lock (usbdev-fixture.c) * wake_lock (syscall/wakeup-pipe.c) * * log_mutex is the one leaf every other entry may hold: a lock anywhere in diff --git a/src/syscall/linux-wire.h b/src/syscall/linux-wire.h index 8c767ebb..7c7e1410 100644 --- a/src/syscall/linux-wire.h +++ b/src/syscall/linux-wire.h @@ -76,6 +76,7 @@ typedef struct { #define LINUX_ECHILD 10 #define LINUX_EOPNOTSUPP 95 #define LINUX_EOVERFLOW 75 +#define LINUX_EREMOTEIO 121 /* Remote I/O error (usbfs URB_SHORT_NOT_OK) */ #define LINUX_ECONNREFUSED 111 #define LINUX_ECONNRESET 104 #define LINUX_ECONNABORTED 103 @@ -141,6 +142,26 @@ typedef struct { #define LINUX_TIOCNOTTY 0x5422 /* -> macOS TIOCNOTTY (same semantics) */ #define LINUX_TIOCGSID 0x5429 /* -> macOS TIOCGSID (same semantics) */ +/* The rest of what do_vfs_ioctl answers for every file before it calls + * f_op->unlocked_ioctl (fs/ioctl.c), beside FIONBIO and FIOASYNC above. Nothing + * here serves them; the numbers exist so a file's own ioctl handler can tell a + * request that reaches it from one that never does. FIONREAD is not among them: + * do_vfs_ioctl hands that one to vfs_ioctl for anything that is not a regular + * file. Neither are the FS_IOC_*FLAGS and FS_IOC_FS*XATTR arms, which answer + * -ENOIOCTLCMD for an inode with no fileattr operations and are retried through + * vfs_ioctl. + */ +#define LINUX_FIOQSIZE 0x5460 /* bytes behind the inode */ +#define LINUX_FIGETBSZ 0x00000002 /* the superblock's block size */ +#define LINUX_FIFREEZE 0xC0045877 /* freeze the filesystem */ +#define LINUX_FITHAW 0xC0045878 /* thaw it */ +#define LINUX_FS_IOC_FIEMAP 0xC020660B /* map an inode's extents */ +#define LINUX_FICLONE 0x40049409 /* reflink a whole file */ +#define LINUX_FICLONERANGE 0x4020940D /* reflink a range of one */ +#define LINUX_FIDEDUPERANGE 0xC0189436 /* dedupe a range against others */ +#define LINUX_FS_IOC_GETFSUUID 0x80111500 /* the superblock's UUID */ +#define LINUX_FS_IOC_GETFSSYSFSPATH 0x80811501 /* its sysfs path */ + /* Serial line control. Linux encodes the argument in the ioctl arg word itself; * macOS has no ioctl form and exposes tcsendbreak/tcdrain/tcflush/tcflow. */ diff --git a/src/syscall/poll.c b/src/syscall/poll.c index aae26b02..6b119459 100644 --- a/src/syscall/poll.c +++ b/src/syscall/poll.c @@ -38,6 +38,7 @@ #include "syscall/proc.h" /* proc_exit_group_requested */ #include "syscall/signal.h" #include "syscall/time.h" /* linux_timespec_valid */ +#include "syscall/usbdev.h" #include "syscall/wakeup-pipe.h" /* The proof in proved/fdset.h bounds nfds by FDSET_MAX_FDS and sizes the @@ -56,6 +57,10 @@ typedef struct { int host_fd; uint16_t word; uint8_t bit_index; + bool usbdev; /* usbfs fd: writability is remapped pipe readability */ + bool usb_want_r; /* usb fd was in the guest's readfds */ + bool usb_want_w; /* usb fd was in the guest's writefds */ + bool disarmed; /* usbfs pipe woke invisibly; interest withdrawn */ short events; short revents; host_fd_ref_t ref; @@ -184,6 +189,90 @@ static uint32_t poll_eval_unpollable(poll_unpollable_t *entries, uint32_t n) return ready; } +/* Whether a wait that already knows whether it found @events should end as + * EINTR, and, through @claimed_out, which half ended it. + * + * The signal is claimed, not just seen: every thread parked on the one wakeup + * byte returns on it together, and only one of them may report EINTR for that + * signal. @claimed_out is set when the claim is what ended the wait, which is + * what tells the caller to leave its temporary mask installed for the delivery. + * + * Ready descriptors outrank the signal. Linux do_poll() and ep_poll() both look + * at signal_pending() only for a pass that found nothing: EINTR would drop the + * events, and a claim would spend the process's one signal on a call that does + * not report it. Returning EINTR while holding a ready fd loses it for good in + * practice -- kqueue and poll re-report it, but the same pending signal is + * still there, so the guest is handed EINTR forever and never drains the fd. + * foot hit exactly that: a SIGCHLD it had a handler for but had not yet run + * left its Wayland socket readable and undelivered, and it spun at 100% CPU + * without ever drawing a window. + * + * @stopped is the caller's own thread-stop answer, because the callers do not + * agree on it: sys_epoll_pwait lets dequeued events outrank a leader-work-only + * stop, while an exit_group stop wins outright everywhere. + */ +static bool poll_wait_interrupted(bool stopped, bool events, bool *claimed_out) +{ + if (stopped || events) + return stopped; + stopped = futex_interrupt_consume(); + *claimed_out = !stopped && signal_claim_interruption(); + return stopped || *claimed_out; +} + +/* Put back the revents ppoll owes for the entries the host never saw: the + * invalid slots POSIX says poll() ignores and resets, and the descriptors macOS + * refused, which select() answered in @unpollable instead. Both are credited to + * the return count, which is why they are stamped together. + */ +static int ppoll_restamp_unseen(uint32_t nfds, + struct pollfd *host_fds, + const bool *need_pollnval, + uint32_t invalid_count, + const poll_unpollable_t *unpollable, + uint32_t unpollable_count, + uint32_t unpollable_ready) +{ + int credited = 0; + if (invalid_count > 0) { + for (uint32_t i = 0; i < nfds; i++) + if (need_pollnval[i]) + host_fds[i].revents = POLLNVAL; + credited += (int) invalid_count; + } + if (unpollable_count > 0) { + for (uint32_t i = 0; i < nfds; i++) + if (unpollable[i].fd >= 0) + host_fds[i].revents = unpollable[i].revents; + credited += (int) unpollable_ready; + } + return credited; +} + +/* A ppoll slice that timed out has to stop re-arming when a pty master has hung + * up or a disarmed usbfs entry has a wake the guest would now see (a + * disconnect, or a completion turning reapable), since the host will never make + * those fds ready. + */ +static bool ppoll_break_pending(uint32_t nfds, + const linux_pollfd_t *guest_fds, + const bool *need_pollnval, + const uint64_t *guest_gen, + const bool *usb_disarmed) +{ + for (uint32_t i = 0; i < nfds; i++) { + if (need_pollnval[i] || guest_fds[i].fd < 0) + continue; + if (proc_pty_master_hung_up(guest_fds[i].fd, guest_gen[i])) + return true; + if (usb_disarmed[i] && + usbdev_poll_guest_revents(guest_fds[i].fd, guest_fds[i].events, + POLLIN) != 0) + return true; + } + return false; +} + int64_t sys_ppoll(guest_t *g, uint64_t fds_gva, uint32_t nfds, @@ -206,6 +295,12 @@ int64_t sys_ppoll(guest_t *g, host_fd_ref_t host_refs[256]; bool need_pollnval[256] = {false}; + /* usbfs fds poll a completion pipe whose host readiness (POLLIN) means + * guest POLLOUT ("URBs reapable"); both directions are remapped through the + * usbdev helpers. + */ + bool usbdev_remap[256] = {false}; + /* Generation pinned per entry in the same fd_lock window as its host fd. * The pty hangup checks below re-resolve the guest fd, so each needs a * witness that the slot still holds the very file this poll resolved; 0 @@ -248,6 +343,14 @@ int64_t sys_ppoll(guest_t *g, host_fds[i].fd = host_fd; host_fds[i].events = guest_fds[i].events; host_fds[i].revents = 0; + if (host_fd >= 0) { + short mapped; + if (usbdev_poll_host_events(guest_fd, guest_fds[i].events, + &mapped)) { + usbdev_remap[i] = true; + host_fds[i].events = mapped; + } + } } /* Log fd types for shutdown diagnostics (verbose only) */ @@ -354,6 +457,15 @@ int64_t sys_ppoll(guest_t *g, int64_t deadline_ms = poll_timeout_ms > 0 ? poll_now_ms() + poll_timeout_ms : -1; + /* Entries whose usbfs completion pipe woke the wait with nothing the guest + * asked to see. Their host interest is withdrawn (the unreaped completion + * keeps the pipe readable, so re-polling it would spin at 100% CPU) and the + * slice loop watches the disconnect map for them instead, mirroring the + * pty-hup slices below. + */ + bool usb_disarmed[256] = {false}; + bool usb_pipe_fired[256] = {false}; + int ret; uint32_t unpollable_ready = 0; ppoll_retry: @@ -390,27 +502,14 @@ int64_t sys_ppoll(guest_t *g, goto ppoll_retry; } - /* Check for process/thread interrupts after waking. The signal is - * claimed, not just seen: every thread in poll() returns on the same - * wakeup byte, and only one of them may report EINTR for it. - * - * Ready descriptors outrank the signal, as in Linux do_poll(), which - * looks at signal_pending() only for a pass that found nothing: EINTR - * would drop the events, and a claim would spend the process's one - * signal on a call that does not report it. sys_epoll_pwait gates its - * check the same way. - */ + /* Interrupts are checked after waking; see poll_wait_interrupted. */ int wake_ready = added_wakeup && ret > 0 && (host_fds[nfds].revents & POLLIN) ? 1 : 0; bool events = ret > wake_ready || unpollable_ready > 0 || invalid_count > 0; - bool stopped = thread_stop_requested(); - if (!stopped && !events) { - stopped = futex_interrupt_consume(); - signal_interrupted = !stopped && signal_claim_interruption(); - } - if (stopped || signal_interrupted) { + if (poll_wait_interrupted(thread_stop_requested(), events, + &signal_interrupted)) { /* Finite wait: part of the guest's timeout is already spent. */ if (deadline_ms >= 0) syscall_restart_forbid(); @@ -421,38 +520,18 @@ int64_t sys_ppoll(guest_t *g, /* Nothing happened within the slice, so re-arm: an indefinite wait * forever, a finite one until its deadline. Only a zero timeout, which - * is a poll rather than a wait, gets a single call. Break out when a - * master has hung up, since the host will never make that fd ready. + * is a poll rather than a wait, gets a single call. */ - if (ret == 0) { - bool hup_pending = false; - for (uint32_t i = 0; i < nfds && !hup_pending; i++) - hup_pending = - !need_pollnval[i] && guest_fds[i].fd >= 0 && - proc_pty_master_hung_up(guest_fds[i].fd, guest_gen[i]); - if (hup_pending) - break; - } + if (ret == 0 && ppoll_break_pending(nfds, guest_fds, need_pollnval, + guest_gen, usb_disarmed)) + break; } while (ret == 0 && unpollable_ready == 0 && poll_timeout_ms != 0 && (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)); - /* POSIX poll() ignores entries with fd < 0 and resets revents to 0, so - * re-stamp POLLNVAL on the invalid slots and credit them to the return - * count. - */ - if (ret >= 0 && invalid_count > 0) { - for (uint32_t i = 0; i < nfds; i++) - if (need_pollnval[i]) - host_fds[i].revents = POLLNVAL; - ret += (int) invalid_count; - } - - if (ret >= 0 && unpollable_count > 0) { - for (uint32_t i = 0; i < nfds; i++) - if (unpollable[i].fd >= 0) - host_fds[i].revents = unpollable[i].revents; - ret += (int) unpollable_ready; - } + if (ret >= 0) + ret += ppoll_restamp_unseen(nfds, host_fds, need_pollnval, + invalid_count, unpollable, unpollable_count, + unpollable_ready); /* A pty master whose guest-side slaves have all closed is hung up, but the * host still sees elfuse's keepalive slave and reports nothing. Stamp @@ -473,6 +552,37 @@ int64_t sys_ppoll(guest_t *g, int saved_errno = errno; + /* Rewrite usbfs entries into guest-visible Linux bits (POLLIN on the + * completion pipe -> POLLOUT|POLLWRNORM; disconnect -> POLLERR|POLLHUP) + * and keep the ready count consistent with the rewritten revents. Runs + * before the re-block decisions below: the pipe's host interest is always + * armed so a disconnect can wake a read-only poll, which means a completion + * wake can map to nothing the guest asked to see. + */ + if (ret >= 0) { + for (uint32_t i = 0; i < nfds; i++) { + if (!usbdev_remap[i] || need_pollnval[i]) + continue; + short before = host_fds[i].revents; + + /* A disarmed entry's pipe interest was withdrawn (fd -1, revents + * 0), so probe with a hypothetical POLLIN and let the disconnect + * and reapable maps decide what the guest sees. Armed entries + * record whether their pipe actually fired BEFORE the writeback + * below overwrites revents; the disarm pass keys off that. + */ + usb_pipe_fired[i] = !usb_disarmed[i] && (before & POLLIN) != 0; + short probe = usb_disarmed[i] ? (short) (before | POLLIN) : before; + short after = usbdev_poll_guest_revents(guest_fds[i].fd, + guest_fds[i].events, probe); + host_fds[i].revents = after; + if (before != 0 && after == 0 && ret > 0) + ret--; + else if (before == 0 && after != 0) + ret++; + } + } + /* Drain the wakeup pipe if it fired, and subtract from count since the * wakeup pipe is not visible to the guest. */ @@ -480,9 +590,49 @@ int64_t sys_ppoll(guest_t *g, wakeup_pipe_drain(); if (ret > 0) ret--; - if (ret == 0 && poll_timeout_ms != 0 && - (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)) - goto ppoll_retry; + } + + /* Re-block when the wake mapped to nothing guest-visible (the wakeup pipe + * or a masked usbfs completion): a spurious 0 from a still-live wait is not + * a poll() outcome Linux produces. A usbfs entry whose completion wake the + * guest cannot see is disarmed first -- its pipe stays readable until the + * URBs are reaped, so leaving it armed would turn the re-block into a + * busy-spin. The slice loop's map probe (disconnect and reapable) stands in + * for the withdrawn interest, and the remap above re-derives the bits from + * the maps once it trips. + */ + if (ret == 0 && poll_timeout_ms != 0 && + (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)) { + for (uint32_t i = 0; i < nfds; i++) { + if (!usbdev_remap[i] || usb_disarmed[i] || need_pollnval[i] || + host_fds[i].fd < 0) + continue; + + /* Withdraw only an interest whose pipe actually fired invisibly + * (that readable byte is what would busy-spin); an armed entry + * still waiting for its first byte keeps its zero-latency wake. + * + * The test is POLLIN and not "anything fired", so a wake carrying + * POLLHUP without POLLIN would leave the entry armed. That wake is + * not producible on the only fd this path sees: it is the read end + * of the readiness pipe, elfuse owns both ends, and macOS reports + * the EOF of a pipe whose write end has closed as POLLIN|POLLHUP + * (0x11) -- a readable EOF, not a bare hangup -- so the test is + * true and the entry disarms. That is the measured run: a sibling + * thread closing the write end under a parked blocking poll gives + * rc=1 revents=0x0011 and returns at the close, 3 runs of 3. A + * parked poll nothing ever wakes is a separate run, and it burns + * nothing either: rc=0 revents=0x0000 at its own 3000 ms timeout, + * under 0.1 ms of CPU. + */ + if (usb_pipe_fired[i] && + usbdev_poll_guest_revents(guest_fds[i].fd, guest_fds[i].events, + POLLIN) == 0) { + usb_disarmed[i] = true; + host_fds[i].fd = -1; + } + } + goto ppoll_retry; } /* Restore the original signal mask, unless a signal this wait claimed ended @@ -568,7 +718,9 @@ static int pselect_fallback_pass(pselect_fallback_t *fb, poll_fds = poll_heap; } for (int i = 0; i < fb->req_count; i++) { - poll_fds[i].fd = fb->unp[i].fd >= 0 ? -1 : fb->reqs[i].host_fd; + poll_fds[i].fd = (fb->unp[i].fd >= 0 || fb->reqs[i].disarmed) + ? -1 + : fb->reqs[i].host_fd; poll_fds[i].events = fb->reqs[i].events; poll_fds[i].revents = 0; } @@ -643,6 +795,215 @@ static void pselect_bits_init(pselect_bits_t *b, } } +/* One usbfs request's guest-visible view of the wait, written into the result + * bitmasks. Completion-pipe readability is guest writability; a disconnect is + * both readable and writable, matching how Linux select folds POLLERR into + * every set the descriptor sits in. + * + * Returns how many bits it set, which select counts once per set rather than + * once per descriptor. On the native path *host_counted comes back as what the + * host's own select() return already charged for this entry: the completion + * pipe sits in the READ set alone, so a wake is worth one however many sets the + * guest sees the descriptor in, and the caller trades that one for these bits. + */ +static int pselect_usb_writeback(const pselect_req_t *req, + bool use_poll_fallback, + const fd_set *read_set, + pselect_bits_t *bits, + int *host_counted) +{ + int word = req->word; + uint64_t bit = BIT64(req->bit_index); + int gfd = (int) req->word * 64 + req->bit_index; + bool ready = use_poll_fallback + ? (req->revents & (POLLIN | POLLHUP | POLLERR)) != 0 + : (RANGE_CHECK(req->host_fd, 0, FD_SETSIZE) && + FD_ISSET(req->host_fd, read_set)); + bool disc = usbdev_fd_disconnected(gfd); + *host_counted = !use_poll_fallback && ready ? 1 : 0; + int ready_bits = 0; + if (bits->w && req->usb_want_w && + (((ready || req->disarmed) && usbdev_fd_reapable(gfd)) || disc)) { + bits->w[word] |= bit; + ready_bits++; + } + if (bits->r && req->usb_want_r && disc) { + bits->r[word] |= bit; + ready_bits++; + } + return ready_bits; +} + +/* Whether a disarmed usbfs entry has a wake the host can no longer deliver: its + * device disconnected, or a completion turned reapable for a write-interested + * entry, either of which arrived after its pipe interest was withdrawn. The + * slice loop breaks on this so the writeback reports it from the maps. + */ +static bool pselect_usb_wake_pending(const pselect_req_t *reqs, int req_count) +{ + for (int i = 0; i < req_count; i++) { + if (!reqs[i].disarmed) + continue; + int gfd = (int) reqs[i].word * 64 + reqs[i].bit_index; + if (usbdev_fd_disconnected(gfd) || + (reqs[i].usb_want_w && usbdev_fd_reapable(gfd))) + return true; + } + return false; +} + +/* Discount usbfs entries the host woke on but the guest cannot see, and return + * how many. + * + * The completion pipe is always armed, so it can end the wait with nothing the + * guest asked about -- completions ready against read-only interest, say, and + * no disconnect. Surfacing those as ready fds would be a lie, so the caller + * subtracts them and re-blocks, exactly as it does for the wakeup pipe. Each + * discounted entry stays disarmed for the rest of the call: its pipe stays + * readable until the URBs are reaped, so re-arming would busy-spin, and the + * slice loop's map check stands in for the withdrawn interest. + */ +static int pselect_usb_discount_invisible(pselect_req_t *reqs, + int req_count, + bool use_poll_fallback, + fd_set *read_set, + fd_set *saved_read) +{ + int discounted = 0; + for (int i = 0; i < req_count; i++) { + if (!reqs[i].usbdev || reqs[i].disarmed) + continue; + bool pipe_ready = + use_poll_fallback + ? (reqs[i].revents & (POLLIN | POLLHUP | POLLERR)) != 0 + : (RANGE_CHECK(reqs[i].host_fd, 0, FD_SETSIZE) && + FD_ISSET(reqs[i].host_fd, read_set)); + if (!pipe_ready) + continue; + int gfd = (int) reqs[i].word * 64 + reqs[i].bit_index; + bool disc = usbdev_fd_disconnected(gfd); + if ((reqs[i].usb_want_w && (usbdev_fd_reapable(gfd) || disc)) || + (reqs[i].usb_want_r && disc)) + continue; + if (!use_poll_fallback) { + FD_CLR(reqs[i].host_fd, read_set); + FD_CLR(reqs[i].host_fd, saved_read); + } + reqs[i].disarmed = true; + reqs[i].revents = 0; + discounted++; + } + return discounted; +} + +/* Classify one admitted request and arm the host-side interest it implies. + * + * A usbfs fd is the one case where what the host is asked to watch is not what + * the guest expressed: select writability means completions are reapable, which + * is the completion pipe's read end turning readable. That interest goes into + * the READ set only, and unconditionally, so a disconnect can wake a read-only + * select; the writeback reports back just what the guest asked for. + */ +static void pselect_req_arm(pselect_req_t *req, + int guest_fd, + const pselect_bits_t *bits, + int word, + uint64_t bit, + fd_set *read_set, + fd_set **read_setp, + fd_set *write_setp, + fd_set *except_setp, + int *max_host_fd) +{ + bool want_r = bits->r && (bits->r[word] & bit); + bool want_w = bits->w && (bits->w[word] & bit); + bool want_e = bits->e && (bits->e[word] & bit); + + short usb_ev = 0; + bool usb = usbdev_poll_host_events( + guest_fd, want_w ? 0x0004 /* POLLOUT */ : 0, &usb_ev); + req->usbdev = usb; + req->usb_want_r = usb && want_r; + req->usb_want_w = usb && want_w; + req->disarmed = false; + req->events = 0; + if (usb) { + req->events = usb_ev; /* POLLIN */ + } else { + if (want_r) + req->events |= POLLIN; + if (want_w) + req->events |= POLLOUT; + if (want_e) + req->events |= POLLPRI; + } + + if (!RANGE_CHECK(req->host_fd, 0, FD_SETSIZE)) + return; + if (req->host_fd > *max_host_fd) + *max_host_fd = req->host_fd; + if (usb) { + if (usb_ev & POLLIN) { + FD_SET(req->host_fd, read_set); + *read_setp = read_set; + } + return; + } + if (want_r) + FD_SET(req->host_fd, *read_setp); + if (want_w) + FD_SET(req->host_fd, write_setp); + if (want_e) + FD_SET(req->host_fd, except_setp); +} + +/* Whether this select has to go through the poll() fallback: fd_set addresses + * only the first FD_SETSIZE descriptors, so one host fd above that bound -- the + * wakeup pipe's included -- takes the whole call off the native path. + */ +static bool pselect_needs_poll_fallback(const pselect_req_t *reqs, + int req_count, + bool added_wakeup, + int wake_fd) +{ + for (int i = 0; i < req_count; i++) + if (!RANGE_CHECK(reqs[i].host_fd, 0, FD_SETSIZE)) + return true; + return added_wakeup && !RANGE_CHECK(wake_fd, 0, FD_SETSIZE); +} + +/* Install the temporary blocked mask pselect6's sixth argument names, which + * points at { const sigset_t *ss; size_t ss_len }. + * + * Returns 0 with @applied_out false when the call asks for no mask, and the + * guest errno otherwise. + */ +static int pselect_install_mask(guest_t *g, + uint64_t sigmask_gva, + uint64_t *saved_out, + bool *applied_out) +{ + if (!sigmask_gva) + return 0; + struct { + uint64_t ss, ss_len; + } ssarg; + if (guest_read_small(g, sigmask_gva, &ssarg, sizeof(ssarg)) < 0) + return -LINUX_EFAULT; + if (ssarg.ss == 0) + return 0; + /* Linux requires ss_len == sizeof(sigset_t). */ + if (ssarg.ss_len != 8) + return -LINUX_EINVAL; + uint64_t new_mask; + if (guest_read_small(g, ssarg.ss, &new_mask, sizeof(new_mask)) < 0) + return -LINUX_EFAULT; + *saved_out = signal_save_blocked(); + signal_set_blocked(new_mask); + *applied_out = true; + return 0; +} + int64_t sys_pselect6(guest_t *g, int nfds, uint64_t readfds_gva, @@ -760,31 +1121,16 @@ int64_t sys_pselect6(guest_t *g, goto pselect_nomem; if (rc < 0) goto pselect_badf; - int host_fd = ref.fd; - reqs[req_count].host_fd = host_fd; + reqs[req_count].host_fd = ref.fd; reqs[req_count].word = (uint16_t) word; reqs[req_count].bit_index = (uint8_t) bit_index; - reqs[req_count].events = 0; reqs[req_count].revents = 0; - if (bits.r && (bits.r[word] & bit)) - reqs[req_count].events |= POLLIN; - if (bits.w && (bits.w[word] & bit)) - reqs[req_count].events |= POLLOUT; - if (bits.e && (bits.e[word] & bit)) - reqs[req_count].events |= POLLPRI; reqs[req_count].ref = ref; + pselect_req_arm(&reqs[req_count], i, &bits, word, bit, + &read_set, &read_setp, write_setp, except_setp, + &max_host_fd); unp[req_count] = (poll_unpollable_t) {.fd = -1}; req_count++; - if (RANGE_CHECK(host_fd, 0, FD_SETSIZE)) { - if (host_fd > max_host_fd) - max_host_fd = host_fd; - if (bits.r && (bits.r[word] & bit)) - FD_SET(host_fd, read_setp); - if (bits.w && (bits.w[word] & bit)) - FD_SET(host_fd, write_setp); - if (bits.e && (bits.e[word] & bit)) - FD_SET(host_fd, except_setp); - } requested &= requested - 1; } } @@ -803,32 +1149,27 @@ int64_t sys_pselect6(guest_t *g, ts.tv_nsec = lts.tv_nsec; } - /* Apply signal mask atomically around the select. Linux pselect6 arg6 - * points to { sigset_t *ss; size_t ss_len }. Save the current blocked mask, - * apply the new one, do the select, then restore the original mask. + /* Finite waits run to this deadline in POLL_WAKE_SLICE_MS slices, like + * ppoll: the slice boundaries are where interrupt requests and -- once a + * usbfs entry has been disarmed below -- the disconnect map get re-checked. + * -1 = no deadline. + */ + int64_t deadline_ms = + has_timeout ? poll_now_ms() + timespec_to_poll_ms(ts.tv_sec, ts.tv_nsec) + : -1; + + /* The mask is installed atomically around the select and put back after it; + * see pselect_install_mask. */ uint64_t saved_blocked = 0; bool mask_applied = false; bool signal_interrupted = false; - bool signal_first = false; - if (sigmask_gva) { - struct { - uint64_t ss, ss_len; - } ssarg; - if (guest_read_small(g, sigmask_gva, &ssarg, sizeof(ssarg)) < 0) - goto pselect_fault; - if (ssarg.ss != 0) { - /* Linux requires ss_len == sizeof(sigset_t). */ - if (ssarg.ss_len != 8) - goto pselect_inval; - uint64_t new_mask; - if (guest_read_small(g, ssarg.ss, &new_mask, sizeof(new_mask)) < 0) - goto pselect_fault; - saved_blocked = signal_save_blocked(); - signal_set_blocked(new_mask); - mask_applied = true; - } - } + int mask_rc = + pselect_install_mask(g, sigmask_gva, &saved_blocked, &mask_applied); + if (mask_rc == -LINUX_EFAULT) + goto pselect_fault; + if (mask_rc < 0) + goto pselect_inval; /* For indefinite selects, add the wakeup pipe so exit_group/futex/signal * requests can interrupt. @@ -849,13 +1190,10 @@ int64_t sys_pselect6(guest_t *g, read_setp = &read_set; } - struct timespec poll_ts = {.tv_sec = 0, .tv_nsec = 200000000L}; /* 200ms */ - struct timespec zero_ts = {0, 0}; - /* Save fd_sets because pselect modifies them in-place to indicate ready - * fds. Without saving/restoring, a retry pass -- an indefinite wait's 200ms - * slice, or the non-blocking pass a pending signal forces -- would operate - * on corrupted (zeroed) fd_sets. + * fds. Without saving/restoring, a retry pass -- a slice of the wait, or + * the non-blocking pass a pending signal forces -- would operate on + * corrupted (zeroed) fd_sets. */ fd_set saved_read, saved_write, saved_except; if (read_setp) @@ -865,15 +1203,8 @@ int64_t sys_pselect6(guest_t *g, if (except_setp) saved_except = except_set; - bool use_poll_fallback = false; - for (int i = 0; i < req_count; i++) { - if (!RANGE_CHECK(reqs[i].host_fd, 0, FD_SETSIZE)) { - use_poll_fallback = true; - break; - } - } - if (added_wakeup && !RANGE_CHECK(wake_fd, 0, FD_SETSIZE)) - use_poll_fallback = true; + bool use_poll_fallback = + pselect_needs_poll_fallback(reqs, req_count, added_wakeup, wake_fd); pselect_fallback_t fb = { .reqs = reqs, @@ -896,16 +1227,17 @@ int64_t sys_pselect6(guest_t *g, except_set = saved_except; /* A signal already pending ends the wait after one non-blocking pass; - * see ppoll. A finite wait has no wakeup pipe, so without this it would - * notice the signal only once its whole timeout had run. + * see ppoll. Parked instead, the wait would sit out a slice before + * looking, and a finite wait has no wakeup pipe to cut that short. */ - signal_first = signal_pending_interruption(NULL); - const struct timespec *wait_ts = - signal_first ? &zero_ts : (has_timeout ? &ts : &poll_ts); + int slice_ms = + signal_pending_interruption(NULL) ? 0 : poll_slice_ms(deadline_ms); + struct timespec slice_ts = {.tv_sec = slice_ms / 1000, + .tv_nsec = (slice_ms % 1000) * 1000000L}; if (use_poll_fallback) { bool restart; - ret = pselect_fallback_pass(&fb, wait_ts, &restart); + ret = pselect_fallback_pass(&fb, &slice_ts, &restart); /* The interrupt predicates below call into the runtime and can * overwrite errno, so an allocation failure leaves the loop here. @@ -916,20 +1248,16 @@ int64_t sys_pselect6(guest_t *g, goto pselect_retry; } else { ret = pselect(max_host_fd + 1, read_setp, write_setp, except_setp, - wait_ts, NULL); + &slice_ts, NULL); } - /* Ready descriptors outrank the signal; see ppoll. */ + /* Ready descriptors outrank the signal; see poll_wait_interrupted. */ bool wake_hit = added_wakeup && ret > 0 && (use_poll_fallback ? fb.wakeup_fired : FD_ISSET(wake_fd, &read_set)); bool events = ret > (wake_hit ? 1 : 0) || fb.ready > 0; - bool stopped = thread_stop_requested(); - if (!stopped && !events) { - stopped = futex_interrupt_consume(); - signal_interrupted = !stopped && signal_claim_interruption(); - } - if (stopped || signal_interrupted) { + if (poll_wait_interrupted(thread_stop_requested(), events, + &signal_interrupted)) { /* Finite wait: part of the guest's timeout is already spent. */ if (has_timeout) syscall_restart_forbid(); @@ -937,7 +1265,11 @@ int64_t sys_pselect6(guest_t *g, errno = EINTR; break; } - } while (ret == 0 && fb.ready == 0 && (!has_timeout || signal_first)); + + if (ret == 0 && pselect_usb_wake_pending(reqs, req_count)) + break; + } while (ret == 0 && fb.ready == 0 && + (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)); int save_errno = errno; @@ -953,7 +1285,14 @@ int64_t sys_pselect6(guest_t *g, FD_CLR(wake_fd, &read_set); if (ret > 0) ret--; - if (ret == 0 && !has_timeout) + if (ret == 0 && (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)) + goto pselect_retry; + } + + if (ret > 0) { + ret -= pselect_usb_discount_invisible( + reqs, req_count, use_poll_fallback, &read_set, &saved_read); + if (ret == 0 && (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)) goto pselect_retry; } @@ -978,10 +1317,18 @@ int64_t sys_pselect6(guest_t *g, if (readfds_gva || writefds_gva || exceptfds_gva) { pselect_bits_t bits; pselect_bits_init(&bits, readfds_gva, writefds_gva, exceptfds_gva); - int ready_bits = 0; + int ready_bits = 0, usb_adjust = 0; for (int i = 0; i < req_count; i++) { int host_fd = reqs[i].host_fd, word = reqs[i].word; uint64_t bit = BIT64(reqs[i].bit_index); + if (reqs[i].usbdev) { + int counted = 0; + int b = pselect_usb_writeback(&reqs[i], use_poll_fallback, + &read_set, &bits, &counted); + ready_bits += b; + usb_adjust += b - counted; + continue; + } if (use_poll_fallback) { /* An entry poll() refused holds its select() answer in unp, the * poll set having left reqs[i].revents at zero. @@ -1014,9 +1361,21 @@ int64_t sys_pselect6(guest_t *g, * select() counts it once per set it is reported in, so a descriptor * ready to read and to write counts twice. The bits just written are * that count for the descriptors the fallback answered. + * + * The native path keeps the host's count for everything else, since + * host select() already counted those per set, and swaps in the same + * per-set count for its usbfs entries alone: the host saw only their + * completion pipes, in the READ set, so a disconnected fd the guest put + * in both sets came back 1 where Linux answers 2. A disarmed entry -- + * one whose host interest this call withdrew -- was charged nothing, so + * its bits are added outright, which is also the credit the wait needs + * when a disconnect lands while it sits disarmed and the host count is + * zero. */ if (use_poll_fallback) ret = ready_bits; + else + ret += usb_adjust; int bytes = nfds_words * 8; if (bits.r && guest_write_small(g, readfds_gva, bits.r, bytes) < 0) @@ -1066,10 +1425,80 @@ int64_t sys_pselect6(guest_t *g, #define LINUX_EPOLLOUT 0x004 #define LINUX_EPOLLERR 0x008 #define LINUX_EPOLLHUP 0x010 +#define LINUX_EPOLLRDNORM 0x040 +#define LINUX_EPOLLWRNORM 0x100 #define LINUX_EPOLLRDHUP 0x2000 #define LINUX_EPOLLET (1U << 31) #define LINUX_EPOLLONESHOT (1U << 30) +/* What "the guest asked to write" means, on every registration. + * + * Every kernel line number in this rule and in the read rule below is Linux + * v6.18: fs/eventpoll.c, fs/pipe.c, net/ipv4/tcp.c, net/unix/af_unix.c, + * net/core/datagram.c and drivers/usb/core/devio.c at that tag, the last of + * which v6.15 through v6.19 all carry unchanged (git blob f6ce6e26e0d4). + * + * epoll reads the same bits poll does -- eventpoll has no mask of its own, and + * ep_item_poll masks the file's revents by epi->event.events (eventpoll.c: + * 1044-1064) -- so a registration that named only EPOLLWRNORM is asking for one + * of the two bits every writable file below raises, and must be woken by it. A + * usbfs fd answers EPOLLOUT|EPOLLWRNORM while completions are reapable + * (devio.c:2833-2847); so does a pipe with room (pipe.c:697) and a writable TCP + * socket (tcp.c:606), while a unix socket and a datagram socket add EPOLLWRBAND + * (af_unix.c:3417, datagram.c:976). Masked by the registration, all four hand + * an EPOLLWRNORM-only guest EPOLLWRNORM, and none of them hands it nothing. + * + * Testing EPOLLOUT alone while poll() and select() answered the pair next door + * was measured on both halves. usbfs: poll with POLLWRNORM alone returns rc=1 + * revents=0x100, epoll_wait with EPOLLWRNORM alone returns rc=0 after its full + * 500 ms timeout, and the same registration with EPOLLOUT returns at once. + * Generic: an EPOLLWRNORM-only ADD of a writable pipe armed no EVFILT_WRITE and + * epoll_wait returned 0 after its full 500 ms timeout, while poll on the same + * fd answered POLLWRNORM at once; EPOLLOUT|EPOLLWRNORM came back as EPOLLOUT + * alone. Both halves therefore read the pair, the way the read half below does + * and the way poll_eval_unpollable's POLL_WRITE_EVENTS always has. + */ +#define LINUX_EPOLL_WRITABLE (LINUX_EPOLLOUT | LINUX_EPOLLWRNORM) + +/* And what "the guest asked to read" means, on every registration. + * + * The same rule read the other way: ep_item_poll masks the file's answer by + * epi->event.events (eventpoll.c:1044-1064), so an EPOLLRDNORM-only + * registration gets whatever the file reports AND EPOLLRDNORM. A pipe, a socket + * or a tty reports EPOLLIN|EPOLLRDNORM when readable, so such a registration + * both fires and is told EPOLLRDNORM rather than EPOLLIN; a usbfs fd reports + * neither bit ever (devio.c:2833-2847), so it never fires there and all that + * reaches it is the EPOLLERR|EPOLLHUP do_epoll_ctl ORs into every mask + * (eventpoll.c:2351). Both outcomes need the read filter registered, which is + * what gating on EPOLLIN alone did not do off the usbfs path: measured, an + * EPOLLRDNORM-only ADD of a readable pipe armed nothing and epoll_wait returned + * 0 after its full 500 ms timeout, while poll on the same fd answered + * POLLRDNORM at once. The poll and select half of this file has read it as the + * pair since it was written (poll_eval_unpollable and the ppoll remap). + * + * EPOLLPRI is deliberately absent, and is not defined anywhere in this file. + * The conditions Linux raises it for -- socket out-of-band data, a sysfs + * attribute poke -- are not modeled by this layer at all: no readiness source + * below produces it, and the poll half only ever passes through a host POLLPRI + * that a host fd raised on its own. So a registration naming only EPOLLPRI arms + * no filter and hears nothing, the EPOLLERR|EPOLLHUP included. Mapping it onto + * EVFILT_EXCEPT would be a guess about which of those two conditions the guest + * meant, so this records what is not modeled instead of half-wiring it. + * + * EPOLLRDBAND (0x080) and EPOLLMSG (0x400) are absent for the same reason and + * with the same consequence. Both are legal for a guest to name -- the mask is + * not validated against a known set -- and the arming gate tests only + * LINUX_EPOLL_READABLE and LINUX_EPOLLRDHUP, so a registration naming either + * one alone arms no filter and never fires. Neither is left out by oversight: + * no readiness source this layer serves raises them. EPOLLRDBAND is the + * priority-band read bit, raised by the same out-of-band machinery as EPOLLPRI, + * which nothing below models; EPOLLMSG is a legacy STREAMS bit that no file's + * poll method sets. Defining either would mean picking a kqueue filter for a + * condition this layer cannot produce, so, as above, the gap is written down + * rather than half-wired. + */ +#define LINUX_EPOLL_READABLE (LINUX_EPOLLIN | LINUX_EPOLLRDNORM) + /* Linux epoll_ctl operations */ #define LINUX_EPOLL_CTL_ADD 1 #define LINUX_EPOLL_CTL_DEL 2 @@ -1109,6 +1538,10 @@ typedef struct { * from then on always, which is what the wait synthesizes. */ bool always_readable; + bool usbdev; /* usbfs fd: EVFILT_READ on the completion pipe is + * reported as EPOLLOUT ("URBs reapable"), never + * EPOLLIN (devio.c:2833-2847). + */ } epoll_reg_t; /* Per-epoll-instance data, stored in fd_table[epfd].dir. Each instance has its @@ -1152,6 +1585,7 @@ static void epoll_reg_deactivate_locked(epoll_instance_t *inst, reg->oneshot_armed = false; reg->pty_master = false; reg->always_readable = false; + reg->usbdev = false; reg->generation = 0; reg->ofd_id = 0; } @@ -1354,12 +1788,55 @@ void epoll_instance_free(void *inst) static inline void epoll_merge_event(linux_epoll_event_t *out, const struct kevent *kev, - const epoll_reg_t *reg) + const epoll_reg_t *reg, + int gfd) { - if (kev->filter == EVFILT_READ) - out->events |= LINUX_EPOLLIN; - if (kev->filter == EVFILT_WRITE) - out->events |= LINUX_EPOLLOUT; + if (reg->usbdev) { + /* Completion-pipe readability alone does not mean "URBs reapable": the + * disconnect wake travels the same pipe, so EPOLLOUT needs the reapable + * map to agree (devio.c poll grants writability only while + * async_completed is non-empty). The usbfs fd signals neither EPOLLIN + * nor EPOLLRDNORM, ever. EOF/errors on the pipe only happen while the + * fd is being torn down -- report the hangup pair Linux uses for a + * removed device. + */ + if (kev->filter == EVFILT_READ && + (reg->events & LINUX_EPOLL_WRITABLE) && usbdev_fd_reapable(gfd)) + out->events |= reg->events & LINUX_EPOLL_WRITABLE; + if (kev->flags & (EV_EOF | EV_ERROR)) + out->events |= LINUX_EPOLLERR | LINUX_EPOLLHUP; + return; + } + if (kev->filter == EVFILT_READ) { + /* Masked by what the registration asked for, the way ep_item_poll masks + * the file's answer: a readable pipe, socket or tty reports + * EPOLLIN|EPOLLRDNORM, so a registration naming one of the two is told + * that one and a registration naming both is told both. + * + * A registration naming NEITHER -- EPOLLRDHUP alone -- is told nothing + * here, and needs no fallback to keep this off a zero-event entry: the + * arming gate gives that case a read filter whose low-water mark no + * readable byte can reach, so the only thing that activates it is + * EV_EOF, which the hangup arm below reports. Linux answers the same + * way while the fd is merely readable -- do_epoll_ctl widens the mask + * by EPOLLERR|EPOLLHUP only, and ep_item_poll masks the pipe's + * EPOLLIN|EPOLLRDNORM by it, so ep_poll waits the caller out and + * returns 0 at the deadline. + */ + out->events |= reg->events & LINUX_EPOLL_READABLE; + } + + if (kev->filter == EVFILT_WRITE) { + /* The same masking as the read half above: a writable pipe or socket + * reports EPOLLOUT|EPOLLWRNORM, so a registration naming one of the two + * is told that one and a registration naming both is told both. Nothing + * arms EVFILT_WRITE without a write bit in the mask, so the fallback is + * unreachable rather than load-bearing; it is here so this cannot be + * the one path that reports an entry with no events at all. + */ + uint32_t want = reg->events & LINUX_EPOLL_WRITABLE; + out->events |= want ? want : LINUX_EPOLLOUT; + } if (kev->flags & EV_EOF) { out->events |= LINUX_EPOLLHUP; if (kev->filter == EVFILT_READ && (reg->events & LINUX_EPOLLRDHUP)) @@ -1630,6 +2107,13 @@ int64_t sys_epoll_ctl(guest_t *g, int epfd, int op, int fd, uint64_t event_gva) bool target_pty_master = proc_pty_master_pts_num(target_host_fd) != UINT32_MAX; + /* usbfs fds always register EVFILT_READ on the completion pipe (EPOLLIN is + * never signaled, but a disconnect must wake any registration). The + * report-side remap is in epoll_merge_event plus the disconnect stamp in + * sys_epoll_pwait. + */ + bool target_usbdev = target_snap.type == FD_USBDEV; + /* Serialize all regs[] access and the paired kqueue mutation against a * concurrent close hook or a sibling epoll_ctl on the same instance. The * kevent() calls below are change-only (non-blocking), so holding the lock @@ -1665,7 +2149,7 @@ int64_t sys_epoll_ctl(guest_t *g, int epfd, int op, int fd, uint64_t event_gva) } /* Remove all filters for this fd. EPOLLRDHUP alone registers - * EVFILT_READ (see ADD path), so check both EPOLLIN and EPOLLRDHUP. + * EVFILT_READ (see ADD path), so check the read pair and EPOLLRDHUP. * Each delete goes in its own kevent call for the reason the MOD path * below already states: a batched call with a NULL eventlist stops at * the first failed change and leaks the survivor, and events names a @@ -1675,12 +2159,19 @@ int64_t sys_epoll_ctl(guest_t *g, int epfd, int op, int fd, uint64_t event_gva) */ { struct kevent del; - if (reg->events & (LINUX_EPOLLIN | LINUX_EPOLLRDHUP)) { + + /* A usbdev registration always holds an EVFILT_READ on the + * completion pipe, whatever the guest asked for, and never holds an + * EVFILT_WRITE: the pipe's readability is what carries "URBs + * reapable", which the wait reports as EPOLLOUT. + */ + if (reg->usbdev || + (reg->events & (LINUX_EPOLL_READABLE | LINUX_EPOLLRDHUP))) { EV_SET(&del, target_host_fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); kevent(epoll_ref.fd, &del, 1, NULL, 0, NULL); } - if (reg->events & LINUX_EPOLLOUT) { + if (!reg->usbdev && (reg->events & LINUX_EPOLL_WRITABLE)) { EV_SET(&del, target_host_fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); kevent(epoll_ref.fd, &del, 1, NULL, 0, NULL); @@ -1734,10 +2225,10 @@ int64_t sys_epoll_ctl(guest_t *g, int epfd, int op, int fd, uint64_t event_gva) linux_epoll_event_t ev = ev_in; /* For MOD, remove old registrations first if they exist in kqueue. - * EPOLLRDHUP alone registers EVFILT_READ (see ADD path), so check both - * EPOLLIN and EPOLLRDHUP (same logic as CTL_DEL). Always attempt the - * deletes even when oneshot_armed: with multi-filter EPOLLONESHOT, only the - * filter that fired was removed by EV_ONESHOT; the other filter is still + * EPOLLRDHUP alone registers EVFILT_READ (see ADD path), so check the read + * pair and EPOLLRDHUP (same logic as CTL_DEL). Always attempt the deletes + * even when oneshot_armed: with multi-filter EPOLLONESHOT, only the filter + * that fired was removed by EV_ONESHOT; the other filter is still * registered and must be cleaned. Issue each delete in its own kevent call * so an ENOENT on one filter does not abort the other -- with a single * batched call and NULL eventlist, kevent stops at the first failed change @@ -1745,11 +2236,14 @@ int64_t sys_epoll_ctl(guest_t *g, int epfd, int op, int fd, uint64_t event_gva) */ if (op == LINUX_EPOLL_CTL_MOD && reg->active) { struct kevent del; - if (reg->events & (LINUX_EPOLLIN | LINUX_EPOLLRDHUP)) { + if (reg->usbdev /* usbdev always holds an EVFILT_READ */ + ? true + : (reg->events & (LINUX_EPOLL_READABLE | LINUX_EPOLLRDHUP)) != + 0) { EV_SET(&del, target_host_fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); kevent(epoll_ref.fd, &del, 1, NULL, 0, NULL); } - if (reg->events & LINUX_EPOLLOUT) { + if (!reg->usbdev && (reg->events & LINUX_EPOLL_WRITABLE)) { EV_SET(&del, target_host_fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); kevent(epoll_ref.fd, &del, 1, NULL, 0, NULL); } @@ -1779,15 +2273,45 @@ int64_t sys_epoll_ctl(guest_t *g, int epfd, int op, int fd, uint64_t event_gva) /* Use (void*)(uintptr_t)fd as udata to identify the guest fd */ void *udata = (void *) (uintptr_t) fd; - if (ev.events & (LINUX_EPOLLIN | LINUX_EPOLLRDHUP)) { + if (target_usbdev) { + /* EVFILT_READ on the completion pipe is always registered, whatever the + * guest asked for: EPOLLOUT (completions reapable) is gated by + * reg->events in epoll_merge_event, and the unmaskable + * EPOLLERR|EPOLLHUP of a disconnect (devio.c:2842-2845) must wake even + * a read-only registration -- the disconnect stamp in sys_epoll_pwait + * only runs for fds kqueue reported. + */ EV_SET(&changes[nchanges], target_host_fd, EVFILT_READ, kflags, 0, 0, udata); nchanges++; - } - if (ev.events & LINUX_EPOLLOUT) { - EV_SET(&changes[nchanges], target_host_fd, EVFILT_WRITE, kflags, 0, 0, - udata); - nchanges++; + } else { + if (ev.events & (LINUX_EPOLL_READABLE | LINUX_EPOLLRDHUP)) { + /* A mask naming EPOLLRDHUP but no readable bit is waiting for the + * EOF, not for data. A plain read filter would wake the wait on + * every readable byte, and the merge above has nothing that + * registration asked for to report, so the wait either invents a + * bit or returns 0 ahead of its timeout -- and Linux does neither, + * it waits the caller out. A low-water mark no readable byte can + * reach draws exactly that line: kqueue holds the filter silent + * while data merely sits in the pipe, and still activates it for + * EV_EOF, which is the hangup this registration is waiting for. + * Measured on pipes and on stream sockets. It also costs no target: + * every type that takes a plain EVFILT_READ here takes it with + * NOTE_LOWAT too, and the types that refuse the low-water mark (a + * directory, /dev/null, /dev/random) refuse the plain filter with + * the same EINVAL, so the refusal paths below see no case they did + * not see before. + */ + bool eof_only = !(ev.events & LINUX_EPOLL_READABLE); + EV_SET(&changes[nchanges], target_host_fd, EVFILT_READ, kflags, + eof_only ? NOTE_LOWAT : 0, eof_only ? INT_MAX : 0, udata); + nchanges++; + } + if (ev.events & LINUX_EPOLL_WRITABLE) { + EV_SET(&changes[nchanges], target_host_fd, EVFILT_WRITE, kflags, 0, + 0, udata); + nchanges++; + } } /* A mask naming no readiness filter registers nothing, so the loop below @@ -1858,6 +2382,7 @@ int64_t sys_epoll_ctl(guest_t *g, int epfd, int op, int fd, uint64_t event_gva) */ reg->events = ev.events; reg->data = ev.data; + reg->usbdev = target_usbdev; reg->generation = target_snap.generation; reg->ofd_id = target_snap.ofd_id; if (!reg->active) @@ -1973,6 +2498,120 @@ static int epoll_collect_hung_up(epoll_instance_t *inst, return n; } +/* The usbfs knotes one sys_epoll_pwait silenced, and what each was silenced + * from. Deliberately not zero-initialized: n is the only field a fresh set + * needs, and the parallel arrays are five kilobytes this syscall would + * otherwise memset on every call. Entry i is written in full before n rises + * past it, so [0, n) is the only range any reader may touch. + */ +typedef struct { + int gfds[256]; + uintptr_t idents[256]; + uint64_t gens[256]; + + /* The writable bits the registration asked for, so the stamp pass reports + * the same pair epoll_merge_event would have (LINUX_EPOLL_WRITABLE). + */ + uint32_t wantout[256]; + int n; +} epoll_mute_set_t; + +/* Whether a muted registration has a wake kqueue can no longer deliver: its + * device disconnected, or a completion turned reapable for one that asked to + * write. The slice loop breaks on this and the stamp pass reports it. + */ +static bool epoll_mute_wake_pending(const epoll_mute_set_t *muted) +{ + for (int i = 0; i < muted->n; i++) { + if (usbdev_fd_disconnected(muted->gfds[i]) || + (muted->wantout[i] && usbdev_fd_reapable(muted->gfds[i]))) + return true; + } + return false; +} + +/* Undo sys_epoll_pwait's mutes on the way out. A muted registration whose + * disconnect was stamped through the map and is EPOLLONESHOT now holds its + * consumed state in oneshot_armed: its knote is deleted instead of re-enabled, + * exactly what EV_ONESHOT would have done on a live fire. Best-effort kevent: + * the fd (and with it the knote) may already be gone. + * + * The lock spans the kevent rather than just the read behind it, the way the + * hangup and mute stamping loops above already decide and act in one locked + * step. Dropping it in between let a concurrent EPOLL_CTL_MOD re-arm the + * registration -- clearing oneshot_armed and re-adding an enabled knote -- + * after consumed had been read true and before the EV_DELETE went out, so the + * delete retired the filter MOD had just installed. The registration was then + * active with no knote behind it, and a disconnected usbfs fd that had a wake + * waiting on its completion pipe reported nothing until the wait timed out. The + * kevent is changelist-only (nevents 0), so it applies and returns without + * blocking and the lock is never held across a wait. + */ +static void epoll_unmute(int kq, + epoll_instance_t *inst, + const epoll_mute_set_t *muted) +{ + for (int i = 0; i < muted->n; i++) { + int gfd = muted->gfds[i]; + pthread_mutex_lock(&inst->lock); + epoll_reg_t *reg = &inst->regs[gfd]; + bool consumed = reg->active && reg->generation == muted->gens[i] && + reg->oneshot_armed; + struct kevent kev; + EV_SET(&kev, muted->idents[i], EVFILT_READ, + consumed ? EV_DELETE : EV_ENABLE, 0, 0, + (void *) (uintptr_t) gfd); + kevent(kq, &kev, 1, NULL, 0, NULL); + pthread_mutex_unlock(&inst->lock); + } +} + +/* Silence the usbfs knotes that fired with nothing the guest could see, and + * return how many this pass added. + * + * EV_DISABLE for a level-triggered registration; a disabled re-add for an + * EPOLLONESHOT one, whose fired EV_ONESHOT already consumed the knote -- a wake + * the guest never saw must not consume the arm silently. Called with the + * instance lock held, for the registration reads. + */ +static int epoll_mute_usb_wakes(int kq, + epoll_instance_t *inst, + const struct kevent *kevents, + int nready, + epoll_mute_set_t *muted) +{ + int muted_now = 0; + for (int i = 0; i < nready && muted->n < (int) ARRAY_SIZE(muted->gfds); + i++) { + int gfd = (int) (uintptr_t) kevents[i].udata; + if (!RANGE_CHECK(gfd, 0, FD_TABLE_SIZE)) + continue; + epoll_reg_t *reg = &inst->regs[gfd]; + if (!reg->active || !reg->usbdev) + continue; + uint16_t kflags; + if (reg->events & LINUX_EPOLLONESHOT) { + kflags = EV_ADD | EV_ONESHOT | EV_DISABLE; + if (reg->events & LINUX_EPOLLET) + kflags |= EV_CLEAR; + } else { + kflags = EV_DISABLE; + } + struct kevent mute; + EV_SET(&mute, kevents[i].ident, EVFILT_READ, kflags, 0, 0, + (void *) (uintptr_t) gfd); + if (kevent(kq, &mute, 1, NULL, 0, NULL) < 0) + continue; /* knote already gone; nothing left to silence */ + muted->gfds[muted->n] = gfd; + muted->idents[muted->n] = kevents[i].ident; + muted->gens[muted->n] = reg->generation; + muted->wantout[muted->n] = reg->events & LINUX_EPOLL_WRITABLE; + muted->n++; + muted_now++; + } + return muted_now; +} + int64_t sys_epoll_pwait(guest_t *g, int epfd, uint64_t events_gva, @@ -2005,7 +2644,6 @@ int64_t sys_epoll_pwait(guest_t *g, uint64_t saved_mask = 0; bool mask_installed = false; bool signal_interrupted = false; - bool signal_first = false; if (sigmask_gva != 0) { uint64_t new_mask; if (guest_read_small(g, sigmask_gva, &new_mask, sizeof(new_mask)) == @@ -2016,13 +2654,13 @@ int64_t sys_epoll_pwait(guest_t *g, } } - /* Convert timeout */ + /* Convert timeout. Finite waits run to a deadline in POLL_WAKE_SLICE_MS + * slices like ppoll's, so interrupt requests, pending pty hangups, and + * muted usbfs registrations (below) are re-checked on slice boundaries. -1 + * = no deadline. + */ bool has_timeout = (timeout_ms >= 0); - struct timespec ts; - if (has_timeout) { - ts.tv_sec = timeout_ms / 1000; - ts.tv_nsec = (timeout_ms % 1000) * 1000000L; - } + int64_t deadline_ms = has_timeout ? poll_now_ms() + timeout_ms : -1; /* A hangup that is already pending must not wait out the caller's timeout. * kqueue will never report it, so a finite epoll_wait would otherwise block @@ -2035,11 +2673,10 @@ int64_t sys_epoll_pwait(guest_t *g, bool hup_ready = epoll_collect_hung_up(inst, &hup_probe, &hup_probe_gen, 1) > 0 || epoll_has_always_readable(inst); - struct timespec zero_ts = {.tv_sec = 0, .tv_nsec = 0}; - /* Collect kqueue events. For indefinite waits, use a short timeout and loop - * so exit_group can interrupt. Cap maxevents before multiply to avoid - * signed integer overflow when maxevents is very large. + /* Collect kqueue events. Waits run in bounded slices and loop so exit_group + * can interrupt. Cap maxevents before multiply to avoid signed integer + * overflow when maxevents is very large. */ if (maxevents > 128) maxevents = 128; @@ -2048,36 +2685,31 @@ int64_t sys_epoll_pwait(guest_t *g, cap = 256; struct kevent kevents[256]; - struct timespec poll_ts = {.tv_sec = 0, .tv_nsec = 200000000L}; /* 200ms */ + /* usbfs registrations whose completion-pipe kevent woke the wait with no + * guest-visible bits. Their knote is muted (unreaped completions keep the + * pipe readable, so re-entering kevent with it armed would either spin or + * return 0 before the timeout -- an outcome Linux ep_poll never produces) + * and the slice loop watches the disconnect map for them instead. Every + * exit path unmutes what was muted. + */ + epoll_mute_set_t muted; + muted.n = 0; + int nready; +epoll_rewait: do { /* A signal already pending ends the wait after one non-blocking pass; - * see ppoll. A finite wait hands kevent its whole timeout, so without - * this it would notice the signal only once that had run. + * see ppoll. Parked instead, the wait would sit out a slice before + * looking. */ - signal_first = !hup_ready && signal_pending_interruption(NULL); - nready = kevent(epoll_ref.fd, NULL, 0, kevents, cap, - (hup_ready || signal_first) - ? &zero_ts - : (has_timeout ? &ts : &poll_ts)); - if (nready > 0) { - } - - /* Evaluated stepwise only to name the one that fired; the guards - * preserve the short-circuit order, so futex_interrupt_consume() still - * runs exactly when it did as a single ||-chain. - */ - /* Ready events outrank an interruption. Linux ep_poll() tests - * ep_events_available() and jumps to send_events before it ever looks - * at signal_pending(), so EINTR is the answer only for a wait that - * produced nothing. - * - * Returning EINTR while holding a ready fd loses it for good in - * practice: kqueue re-reports it on the next call, but the same pending - * signal is still there, so the guest is handed EINTR forever and never - * drains the fd. foot hit exactly that -- a SIGCHLD it had a handler - * for but had not yet run left its Wayland socket readable and - * undelivered, and it spun at 100% CPU without ever drawing a window. + int slice_ms = (hup_ready || signal_pending_interruption(NULL)) + ? 0 + : poll_slice_ms(deadline_ms); + struct timespec slice_ts = {.tv_sec = slice_ms / 1000, + .tv_nsec = (slice_ms % 1000) * 1000000L}; + nready = kevent(epoll_ref.fd, NULL, 0, kevents, cap, &slice_ts); + + /* Ready events outrank an interruption; see poll_wait_interrupted. * * exit_group still wins outright: the process is going away and there * is nothing to deliver events to. An execve handed to this leader does @@ -2087,14 +2719,9 @@ int64_t sys_epoll_pwait(guest_t *g, * The handoff runs at the top of the run loop whether this returns * events or EINTR, so letting the events win costs it nothing. */ - bool interrupted = thread_stop_requested() && - !(nready > 0 && thread_stop_is_leader_work_only()); - if (!interrupted && nready <= 0) { - interrupted = futex_interrupt_consume(); - signal_interrupted = !interrupted && signal_claim_interruption(); - interrupted = interrupted || signal_interrupted; - } - if (interrupted) { + bool stopped = thread_stop_requested() && + !(nready > 0 && thread_stop_is_leader_work_only()); + if (poll_wait_interrupted(stopped, nready > 0, &signal_interrupted)) { /* Finite wait: part of the guest's timeout is already spent. */ if (has_timeout) syscall_restart_forbid(); @@ -2103,30 +2730,32 @@ int64_t sys_epoll_pwait(guest_t *g, break; } - /* An indefinite wait re-arms on a 200ms slice; break out when a master - * hung up during one, since kqueue will never make that fd ready. + /* A wait re-arms on its slice; break out when a master hung up or a + * muted usbfs device disconnected during one, since kqueue will never + * make those fds ready. */ - if (nready == 0 && !has_timeout) { + if (nready == 0) { hup_ready = epoll_collect_hung_up(inst, &hup_probe, &hup_probe_gen, 1) > 0; - if (hup_ready) + if (hup_ready || epoll_mute_wake_pending(&muted)) break; } - } while (nready == 0 && (!has_timeout || signal_first)); - - int saved_errno = errno; - - /* Restore the original signal mask; see ppoll for the claimed signal. */ - if (mask_installed) { - if (signal_interrupted) - signal_defer_restore_blocked(saved_mask); - else - signal_restore_blocked(saved_mask); - } + } while (nready == 0 && + (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)); if (nready < 0) { + int saved_errno = errno; + + /* Restore the mask; see ppoll for the claimed signal. */ + if (mask_installed) { + if (signal_interrupted) + signal_defer_restore_blocked(saved_mask); + else + signal_restore_blocked(saved_mask); + } errno = saved_errno; ret = linux_errno(); + epoll_unmute(epoll_ref.fd, inst, &muted); host_fd_ref_close(&epoll_ref); epoll_instance_release(inst); return ret; @@ -2138,8 +2767,12 @@ int64_t sys_epoll_pwait(guest_t *g, * the same epoll_data value. */ linux_epoll_event_t out[256]; - /* Parallel array tracking which guest FD each output entry represents. */ + + /* Parallel arrays tracking which guest FD each output entry represents and + * the host ident that produced it (the mute arm below needs the ident). + */ uint16_t out_gfds[256]; + uintptr_t out_idents[256]; int16_t out_index[FD_TABLE_SIZE]; int nout = 0; @@ -2182,18 +2815,23 @@ int64_t sys_epoll_pwait(guest_t *g, epoll_reg_t *reg = &inst->regs[gfd]; int idx = out_index[gfd]; - if (idx >= 0) { - epoll_merge_event(&out[idx], &kevents[i], reg); - continue; + if (idx < 0) { + idx = nout++; + out_index[gfd] = idx; + out_gfds[idx] = gfd; + out_idents[idx] = kevents[i].ident; + out[idx].events = 0; + out[idx]._pad = 0; + out[idx].data = reg->data; } + epoll_merge_event(&out[idx], &kevents[i], reg, gfd); - idx = nout++; - out_index[gfd] = idx; - out_gfds[idx] = gfd; - out[idx].events = 0; - out[idx]._pad = 0; - out[idx].data = reg->data; - epoll_merge_event(&out[idx], &kevents[i], reg); + /* A disconnected usbfs device reports the hangup pair alongside any + * remaining reapable completions (devio.c:2842-2845; EPOLLHUP and + * EPOLLERR are unmaskable). + */ + if (reg->usbdev && usbdev_fd_disconnected(gfd)) + out[idx].events |= LINUX_EPOLLERR | LINUX_EPOLLHUP; } /* Stamp EPOLLIN for the registrations whose read readiness kqueue cannot @@ -2208,7 +2846,7 @@ int64_t sys_epoll_pwait(guest_t *g, epoll_reg_t *areg = &inst->regs[gfd]; if (!areg->always_readable || !areg->active || areg->oneshot_armed) continue; - if (!(areg->events & LINUX_EPOLLIN)) + if (!(areg->events & LINUX_EPOLL_READABLE)) continue; int idx = out_index[gfd]; if (idx < 0) { @@ -2219,7 +2857,7 @@ int64_t sys_epoll_pwait(guest_t *g, out[idx]._pad = 0; out[idx].data = areg->data; } - out[idx].events |= LINUX_EPOLLIN; + out[idx].events |= areg->events & LINUX_EPOLL_READABLE; } } @@ -2250,6 +2888,7 @@ int64_t sys_epoll_pwait(guest_t *g, idx = nout++; out_index[gfd] = idx; out_gfds[idx] = gfd; + out_idents[idx] = 0; out[idx].events = 0; out[idx]._pad = 0; out[idx].data = inst->regs[gfd].data; @@ -2257,6 +2896,85 @@ int64_t sys_epoll_pwait(guest_t *g, out[idx].events |= LINUX_EPOLLHUP; } + /* Stamp what the muted registrations can no longer learn from kqueue: their + * knote is silenced, so the slice loop's map-driven break lands here with + * nready == 0. A disconnect stamps the unmaskable hangup pair; a completion + * that became reapable after the mute (the mute races a concurrent reap + * emptying the list) stamps the writable bits it asked for. Re-validated + * like the hangups above; the generation match rejects a DEL + re-ADD since + * the mute. + */ + for (int i = 0; i < muted.n; i++) { + int gfd = muted.gfds[i]; + bool disc = usbdev_fd_disconnected(gfd); + uint32_t wr = muted.wantout[i] & inst->regs[gfd].events; + bool reap = wr != 0 && usbdev_fd_reapable(gfd); + if (!disc && !reap) + continue; + if (!inst->regs[gfd].active || inst->regs[gfd].oneshot_armed || + inst->regs[gfd].generation != muted.gens[i]) + continue; + int idx = out_index[gfd]; + if (idx < 0) { + if (nout >= maxevents) + break; + idx = nout++; + out_index[gfd] = idx; + out_gfds[idx] = gfd; + out_idents[idx] = muted.idents[i]; + out[idx].events = 0; + out[idx]._pad = 0; + out[idx].data = inst->regs[gfd].data; + } + if (disc) + out[idx].events |= LINUX_EPOLLERR | LINUX_EPOLLHUP; + if (reap) + out[idx].events |= wr; + } + + /* An always-armed usbfs completion pipe can report a kevent that maps to no + * guest-visible bits (completions ready, EPOLLOUT not requested, no + * disconnect). Drop those entries rather than hand the guest events == 0; + * the mute arm below then re-enters the wait rather than returning a 0 + * count before the timeout. + */ + int ndropped = 0; + { + int w = 0; + for (int i = 0; i < nout; i++) { + if (out[i].events == 0) { + out_index[out_gfds[i]] = -1; + ndropped++; + continue; + } + if (w != i) { + out[w] = out[i]; + out_gfds[w] = out_gfds[i]; + out_idents[w] = out_idents[i]; + out_index[out_gfds[w]] = (int16_t) w; + } + w++; + } + nout = w; + } + + /* Drop-compaction zeroed a positive nready: every wake was a masked usbfs + * completion. Mute the knotes that fired and re-enter the slice loop + * instead of returning 0 before the timeout (Linux ep_poll never does; a + * guest treating 0 as its timeout would act early). The mute is EV_DISABLE + * for a level-triggered registration, and a disabled re-add for an + * EPOLLONESHOT one -- its fired EV_ONESHOT already consumed the knote, and + * a wake the guest never saw must not consume the arm silently. + */ + if (nout == 0 && ndropped > 0 && + (deadline_ms < 0 || poll_slice_ms(deadline_ms) > 0)) { + if (epoll_mute_usb_wakes(epoll_ref.fd, inst, kevents, nready, &muted) > + 0) { + pthread_mutex_unlock(&inst->lock); + goto epoll_rewait; + } + } + /* Mark EPOLLONESHOT FDs as armed (fired but waiting for MOD re-arm). kqueue * already removed the event (EV_ONESHOT), so poll emulation marks the * registration as oneshot_armed to allow MOD but prevent further event @@ -2272,6 +2990,18 @@ int64_t sys_epoll_pwait(guest_t *g, pthread_mutex_unlock(&inst->lock); + /* Restore the original signal mask after the wait, kept installed across + * the mute re-entry above. See ppoll for the claimed signal. + */ + if (mask_installed) { + if (signal_interrupted) + signal_defer_restore_blocked(saved_mask); + else + signal_restore_blocked(saved_mask); + } + + epoll_unmute(epoll_ref.fd, inst, &muted); + /* Write results to guest */ if (nout > 0) { for (int i = 0; i < nout; i++) diff --git a/src/syscall/usbdev-fixture-stub.c b/src/syscall/usbdev-fixture-stub.c new file mode 100644 index 00000000..9dd894c5 --- /dev/null +++ b/src/syscall/usbdev-fixture-stub.c @@ -0,0 +1,72 @@ +/* + * The USB fixture seam, answered by a build that carries no fixture + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * usbdev.c asks the seam in syscall/usbdev-fixture.h whether a fixture models + * the device it is about to open, and takes the IOKit registry path when the + * answer is no. That question is worth asking unconditionally, because the + * answer is what keeps the fixture out of the paths it must not touch; the + * model behind a yes is not, because only an assertion wants a device that + * echoes back what was written to it. So the question is compiled into every + * binary and the model is not: this file says no to all of it, and + * syscall/usbdev-fixture.c is linked in place of it when USB_LOOPBACK_FIXTURE + * asks for the model (mk/config.mk). + * + * Answering at the link rather than at the call sites is what leaves usbdev.c + * one program in both builds. Not one of its branches is conditionally + * compiled, so the fixture cannot drift into code the default build never + * compiles, and the whole cost of the seam in a shipped binary is these seven + * bodies plus the calls that reach them: one per device open, one per interface + * claim, one per disconnect watch, and one as the event thread starts. + */ + +#include +#include + +#include +#include +#include + +#include "syscall/linux-wire.h" +#include "syscall/usbdev-fixture.h" + +bool usbdev_fixture_loopback(void) +{ + return false; +} + +bool usbdev_fixture_has_device(uint32_t location_id, unsigned vid, unsigned pid) +{ + return false; +} + +void usbdev_fixture_bind_loop(CFRunLoopRef loop) {} + +/* -ENODEV, not -ENOSYS or a crash: the caller asked for a device this build + * does not have, which is the same answer the model gives for every location + * but its own, and the answer Linux reports for a device that is gone. + */ +int64_t usbdev_fixture_open_device(uint32_t location_id, + unsigned vid, + unsigned pid, + IOUSBDeviceInterface650 ***out) +{ + return -LINUX_ENODEV; +} + +int64_t usbdev_fixture_open_iface(uint32_t location_id, + unsigned ifnum, + IOUSBInterfaceInterface800 ***out) +{ + return -LINUX_ENODEV; +} + +void usbdev_fixture_watch(uint32_t location_id, + IOServiceInterestCallback cb, + void *refcon) +{ +} + +void usbdev_fixture_unwatch(void *refcon) {} diff --git a/src/syscall/usbdev-fixture.c b/src/syscall/usbdev-fixture.c new file mode 100644 index 00000000..98a91b23 --- /dev/null +++ b/src/syscall/usbdev-fixture.c @@ -0,0 +1,1472 @@ +/* + * A loopback device behind the IOKit COM seam (ELFUSE_USB_FIXTURE=loopback) + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Why this exists: the async URB engine in usbdev.c cannot be reached without + * an IOKit service behind the node, and the other ELFUSE_USB_FIXTURE models + * have none, so submit, complete, reap, the per-endpoint queue, the disconnect + * drain and every readiness path in poll.c had no in-tree lane at all. IOKit + * publishes no loopback device to borrow, so the seam is placed at the + * narrowest point that still leaves all of that real: the two COM vtables. + * Nothing above them changes, and the only calls replaced are the ones that + * would have gone to the wire plus the callbacks that would have come back from + * it. + * + * The script grammar (ELFUSE_USB_LOOPBACK, or the guest's 0xF1 command below): + * + * script := rule (';' rule)* + * rule := 'ep' ':' step (',' step)* + * step := name ['(' number ')'] ['*' repeat] + * + * Endpoint 00 is the default control pipe. A rule's steps are consumed one per + * transfer on that endpoint and the last one repeats forever; an endpoint with + * no rule behaves as 'ok'. A delay step is not an outcome: it sets the delay + * that the next outcome in the same rule waits before completing. + * + * ok / ok(n) kIOReturnSuccess. arg0 is the transferred count: the whole + * buffer, or n verbatim, which is how an over-reporting device + * (n > the submitted length) reaches the actual_length clamp. + * short(n) kIOReturnUnderrun with arg0 = n. usbfs treats a short read as + * success, so this is the URB_SHORT_NOT_OK input as well. + * stall kIOUSBPipeStalled, the -EPIPE row. + * timeout kIOUSBTransactionTimeout, the -ETIMEDOUT row. + * nodev kIOReturnNoDevice: -ENODEV plus the disconnect mark the + * completion callback makes from that code alone. + * err(code) any IOReturn verbatim; with no argument, kIOReturnIOError, + * which is the map's default -EPROTO row. + * refuse(code) the submit entry point itself returns code instead of + * scheduling anything, which is the start-gate path where the + * URB-status map has to be used rather than the syscall map. + * With no argument, kIOReturnNotOpen. + * never accepted and never completed until something aborts the pipe, + * which is what DISCARDURB and every kill do. That is faithful + * to AbortPipe, and it is also why never cannot reach the drain + * deadline: the abort it answers promptly is the drain. + * wedge(ms) never, plus an abort that takes ms to land (default 2500, + * past the engine's 2 s drain deadline). This is the transfer + * the deadline and the orphaning exist for, and the only step + * that reaches them. + * delay(ms) the next outcome completes ms milliseconds later. + * terminate the device terminates, with no completion for the transfer + * that triggered it and kIOReturnNoDevice for every later start. + * That is what the board actually does: three URBs outstanding + * at terminate produced zero callbacks, and CAP_REAP_AFTER_ + * DISCONNECT's self-issued kill exists for exactly that. + * zlpfail the transfer succeeds and the terminating zero-length write + * the completion callback then issues comes back stalled. + * + * Vendor control requests on the fixture device itself are the control plane, + * so a guest can drive many scenarios in one process. They are answered inside + * DeviceRequestTO, which is the synchronous ioctl path, so they never disturb + * the async script: + * + * 0xC0 0xF0 read the wire log from record wValue, 32 bytes per record + * 0x40 0xF1 replace the script with the request payload + * 0x40 0xF2 terminate the device in wValue milliseconds. A wIndex naming + * either bit below arms no terminate at all: it leaves the device + * in place and breaks one call apiece, which is how a departure + * only one IOKit entry point can see is modeled. Bit 1 tears down + * the claimed interface's pipes, so GetPipeProperties alone + * answers NoDevice: the device vanishing between the + * GetNumEndpoints that sizes a pipe map and the calls that fill it + * in. Bit 2 makes AbortPipe and USBDeviceAbortPipeZero answer + * NoDevice, which is the abort a teardown drain cannot get issued + * at all. Both bits are cleared by 0xF3. + * 0x40 0xF3 clear the wire log and its clock, the loopback data stash, the + * pending zlpfail steps, the 0xF4 counter and both 0xF2 wIndex + * bits above + * 0xC0 0xF4 read the counters the fixture keeps about the layer above it + * rather than about the wire, 4 bytes little-endian. One so far: + * how many times the device interface was Released with a control + * transfer still outstanding on it. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "debug/log.h" +#include "runtime/usb-fixture.h" +#include "syscall/linux-wire.h" +#include "syscall/usbdev-fixture.h" + +/* mode */ + +bool usbdev_fixture_loopback(void) +{ + static _Atomic int cached = -1; + int v = atomic_load_explicit(&cached, memory_order_relaxed); + if (v < 0) { + const char *env = getenv("ELFUSE_USB_FIXTURE"); + v = (env && !strcmp(env, "loopback")) ? 1 : 0; + atomic_store_explicit(&cached, v, memory_order_relaxed); + } + return v != 0; +} + +/* script */ + +typedef enum { + FX_DELAY = 0, + FX_OK, + FX_SHORT, + FX_STALL, + FX_TIMEOUT, + FX_NODEV, + FX_ERR, + FX_REFUSE, + FX_NEVER, + FX_WEDGE, + FX_TERMINATE, + FX_ZLPFAIL, + FX_ABORTED, /* not spellable: what an abort turns a transfer into */ +} fx_act_t; + +typedef struct { + uint8_t act; + bool has_arg; + uint32_t arg; +} fx_step_t; + +#define FX_MAX_RULES 16 +#define FX_MAX_STEPS 32 + +/* Past usbdev.c's 2 s drain deadline with room for a loaded machine. */ +#define FX_WEDGE_ABORT_MS 2500 + +typedef struct { + bool used; + uint8_t ep; + int nsteps; + int cursor; + fx_step_t steps[FX_MAX_STEPS]; +} fx_rule_t; + +/* One 32-byte wire-log record, serialized little-endian for the guest. The + * count is written at submit and patched at completion, so a transfer that is + * still in flight (or never completes) is in the log too. + */ +typedef struct { + uint8_t kind; /* 1 async, 2 sync pipe, 3 zero-length write, 4 control */ + uint8_t ep; + uint8_t flags; /* bit 0: IN */ + uint8_t + concurrent; /* transfers in flight on this endpoint, this included */ + uint32_t requested; + uint32_t actual; + uint32_t ioreturn; + uint32_t seq; + uint32_t start_ms; + uint8_t data[8]; +} fx_rec_t; + +#define FX_REC_BYTES 32 +#define FX_LOG_MAX 128 +#define FX_STASH_MAX 4096 +#define FX_MAX_XFERS 64 +#define FX_MAX_WATCH 32 + +typedef struct { + uint32_t location_id; + fx_rule_t rules[FX_MAX_RULES]; + bool terminated; + uint8_t stash[FX_STASH_MAX]; + uint32_t stash_len; + fx_rec_t log[FX_LOG_MAX]; + unsigned nlog; + unsigned seq; + unsigned inflight_ep[256]; + bool zlp_fail[256]; + bool pipes_gone; /* GetPipeProperties alone answers NoDevice */ + + /* AbortPipe and USBDeviceAbortPipeZero answer kIOReturnNoDevice and cancel + * nothing. Its own fact, and not d->terminated, for the reason the seam + * invariant at fx_create_iface_iterator gives: an abort after the device + * leaves is precisely what the disconnect drain issues and IOKit lands it, + * so tying these to the terminate would leave every 'never' transfer + * outstanding for ever and no drain would finish. What this models is the + * narrower answer -- the user client is gone, so the abort aborts nothing + * -- which is the one state usbdev_do_discardurb had no way to reach and + * the one it used to discard. + */ + bool aborts_gone; + + /* Device handles released while a transfer this fixture still owns is + * outstanding on the default control pipe. IOKit would answer a later + * callback against a freed user client; the fixture frees a COM wrapper + * nothing dereferences again, so the use-after-free itself is unreachable + * here and this counter is what stands in for it. Read back through + * FX_CMD_STATS, so a scenario can assert on it after the fd is gone. + */ + uint32_t dev_release_inflight; + uint64_t t0_ms; + struct { + bool used; + IOServiceInterestCallback cb; + void *refcon; + } watch[FX_MAX_WATCH]; +} fx_dev_t; + +typedef struct { + bool used; + fx_dev_t *dev; + uint8_t ep; + bool is_in; + bool abort; + void *buf; + uint32_t size; + uint8_t act; + bool has_arg; + uint32_t arg; + IOAsyncCallback1 cb; + void *refcon; + CFRunLoopTimerRef timer; + int logidx; +} fx_xfer_t; + +/* One device, one lock. A leaf: the timer callback takes it, drops it, and only + * then calls into usbdev.c, so async_lock is never held beneath it and it is + * never held beneath async_lock across a callback. See internal.h. + */ +static pthread_mutex_t usbdev_fixture_lock = PTHREAD_MUTEX_INITIALIZER; +static fx_dev_t fx_dev; +static bool fx_dev_ready; +static fx_xfer_t fx_xfers[FX_MAX_XFERS]; +static CFRunLoopRef fx_loop; + +static uint64_t fx_now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t) ts.tv_sec * 1000u + (uint64_t) (ts.tv_nsec / 1000000); +} + +static const char *fx_skip_ws(const char *p) +{ + while (*p == ' ' || *p == '\t' || *p == '\n') + p++; + return p; +} + +static bool fx_word(const char **pp, const char *word) +{ + size_t n = strlen(word); + if (strncmp(*pp, word, n) != 0) + return false; + *pp += n; + return true; +} + +/* Parse one step. + * + * Returns false on anything unrecognized, which the caller reports rather than + * silently running a different script than it was given. + */ +static bool fx_parse_step(const char **pp, fx_step_t *st, int *repeat) +{ + const char *p = fx_skip_ws(*pp); + st->has_arg = false; + st->arg = 0; + *repeat = 1; + if (fx_word(&p, "delay")) + st->act = FX_DELAY; + else if (fx_word(&p, "ok")) + st->act = FX_OK; + else if (fx_word(&p, "short")) + st->act = FX_SHORT; + else if (fx_word(&p, "stall")) + st->act = FX_STALL; + else if (fx_word(&p, "timeout")) + st->act = FX_TIMEOUT; + else if (fx_word(&p, "nodev")) + st->act = FX_NODEV; + else if (fx_word(&p, "err")) + st->act = FX_ERR; + else if (fx_word(&p, "refuse")) + st->act = FX_REFUSE; + else if (fx_word(&p, "never")) + st->act = FX_NEVER; + else if (fx_word(&p, "wedge")) + st->act = FX_WEDGE; + else if (fx_word(&p, "terminate")) + st->act = FX_TERMINATE; + else if (fx_word(&p, "zlpfail")) + st->act = FX_ZLPFAIL; + else + return false; + if (*p == '(') { + char *end = NULL; + unsigned long v = strtoul(p + 1, &end, 0); + if (!end || *end != ')') + return false; + st->has_arg = true; + st->arg = (uint32_t) v; + p = end + 1; + } + if (*p == '*') { + char *end = NULL; + unsigned long v = strtoul(p + 1, &end, 10); + if (!end || end == p + 1 || v == 0) + return false; + *repeat = (int) (v > FX_MAX_STEPS ? FX_MAX_STEPS : v); + p = end; + } + *pp = fx_skip_ws(p); + return true; +} + +/* Replace the whole rule set. Called with the lock held. */ +static void fx_script_load(fx_dev_t *d, const char *script) +{ + memset(d->rules, 0, sizeof(d->rules)); + if (!script || !*script) + return; + const char *p = script; + int nrules = 0; + while (*p && nrules < FX_MAX_RULES) { + p = fx_skip_ws(p); + if (!fx_word(&p, "ep")) { + log_warn("usbdev fixture: script wants ep: at \"%s\"", p); + return; + } + char *end = NULL; + unsigned long ep = strtoul(p, &end, 16); + if (!end || end == p || ep > 0xff || *end != ':') { + log_warn("usbdev fixture: bad endpoint in script at \"%s\"", p); + return; + } + p = end + 1; + fx_rule_t *r = &d->rules[nrules++]; + r->used = true; + r->ep = (uint8_t) ep; + r->nsteps = 0; + r->cursor = 0; + for (;;) { + fx_step_t st; + int rep = 1; + if (!fx_parse_step(&p, &st, &rep)) { + log_warn("usbdev fixture: bad step in script at \"%s\"", p); + r->used = false; + return; + } + for (int i = 0; i < rep && r->nsteps < FX_MAX_STEPS; i++) + r->steps[r->nsteps++] = st; + if (*p != ',') + break; + p++; + } + if (*p == ';') + p++; + else if (*p) + break; + } +} + +/* The next outcome for ep, plus the delay the steps ahead of it asked for. The + * cursor stops on the last outcome, so it repeats. + */ +static void fx_next(fx_dev_t *d, uint8_t ep, fx_step_t *out, uint32_t *delay_ms) +{ + out->act = FX_OK; + out->has_arg = false; + out->arg = 0; + *delay_ms = 0; + fx_rule_t *r = NULL; + for (int i = 0; i < FX_MAX_RULES; i++) { + if (d->rules[i].used && d->rules[i].ep == ep) { + r = &d->rules[i]; + break; + } + } + if (!r || r->nsteps == 0) + return; + for (;;) { + int i = r->cursor; + if (i >= r->nsteps) { + /* Past the end: repeat the last outcome, with the delay that came + * with it. + */ + for (int k = r->nsteps - 1; k >= 0; k--) { + if (r->steps[k].act != FX_DELAY) { + *out = r->steps[k]; + if (k > 0 && r->steps[k - 1].act == FX_DELAY) + *delay_ms = r->steps[k - 1].arg; + return; + } + } + return; + } + if (r->steps[i].act == FX_DELAY) { + *delay_ms += r->steps[i].arg; + r->cursor++; + continue; + } + *out = r->steps[i]; + r->cursor++; + return; + } +} + +/* wire log */ + +static int fx_log_open(fx_dev_t *d, + uint8_t kind, + uint8_t ep, + bool is_in, + uint32_t requested, + const void *payload, + unsigned concurrent) +{ + if (d->nlog >= FX_LOG_MAX) + return -1; + if (d->t0_ms == 0) + d->t0_ms = fx_now_ms(); + int idx = (int) d->nlog++; + fx_rec_t *r = &d->log[idx]; + memset(r, 0, sizeof(*r)); + r->kind = kind; + r->ep = ep; + r->flags = is_in ? 1u : 0u; + r->concurrent = (uint8_t) (concurrent > 255 ? 255 : concurrent); + r->requested = requested; + r->ioreturn = 0xffffffffu; /* still in flight */ + r->seq = ++d->seq; + r->start_ms = (uint32_t) (fx_now_ms() - d->t0_ms); + if (payload && requested) + memcpy(r->data, payload, requested < 8 ? requested : 8); + return idx; +} + +static void fx_log_close(fx_dev_t *d, + int idx, + uint32_t actual, + IOReturn result, + const void *payload) +{ + if (idx < 0 || idx >= (int) d->nlog) + return; + fx_rec_t *r = &d->log[idx]; + r->actual = actual; + r->ioreturn = (uint32_t) result; + if (payload && (r->flags & 1u)) { + /* actual is whatever the script said, and a script may report more than + * the transfer asked for -- ok(16) against a four-byte IN URB. payload + * is the submitted buffer, so the peek is bounded by what was requested + * as well as by the field: reading to actual walked off the allocation. + */ + uint32_t n = actual < r->requested ? actual : r->requested; + if (n > 8) + n = 8; + memcpy(r->data, payload, n); + } +} + +static void fx_put32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t) v; + p[1] = (uint8_t) (v >> 8); + p[2] = (uint8_t) (v >> 16); + p[3] = (uint8_t) (v >> 24); +} + +static void fx_rec_serialize(const fx_rec_t *r, uint8_t *out) +{ + memset(out, 0, FX_REC_BYTES); + out[0] = r->kind; + out[1] = r->ep; + out[2] = r->flags; + out[3] = r->concurrent; + fx_put32(out + 4, r->requested); + fx_put32(out + 8, r->actual); + fx_put32(out + 12, r->ioreturn); + fx_put32(out + 16, r->seq); + fx_put32(out + 20, r->start_ms); + memcpy(out + 24, r->data, 8); +} + +/* loopback data */ + +/* What an IN transfer hands back: the bytes the last OUT wrote, or a + * deterministic ramp when nothing has been written yet. + * + * Returns the count. + */ +static uint32_t fx_fill_in(fx_dev_t *d, void *buf, uint32_t size) +{ + if (!buf || !size) + return 0; + uint8_t *b = buf; + if (d->stash_len) { + uint32_t n = size < d->stash_len ? size : d->stash_len; + memcpy(b, d->stash, n); + return n; + } + for (uint32_t i = 0; i < size; i++) + b[i] = (uint8_t) (0xa0u + (i & 0x0fu)); + return size; +} + +static void fx_store_out(fx_dev_t *d, const void *buf, uint32_t size) +{ + if (!buf || !size) + return; + uint32_t n = size > FX_STASH_MAX ? FX_STASH_MAX : size; + memcpy(d->stash, buf, n); + d->stash_len = n; +} + +/* completions */ + +static void fx_timer_cb(CFRunLoopTimerRef timer, void *info); +static void fx_terminate_cb(CFRunLoopTimerRef timer, void *info); + +/* Arm a one-shot timer on the event runloop (lock held). Timers are never + * canceled: an abort moves the fire date forward and the outcome is decided + * inside the callback, so a completion that was already dispatched cannot be + * completed twice. + */ +static CFRunLoopTimerRef fx_arm(uint32_t delay_ms, + CFRunLoopTimerCallBack fn, + void *info) +{ + if (!fx_loop) + return NULL; + CFRunLoopTimerContext ctx = {0, info, NULL, NULL, NULL}; + CFRunLoopTimerRef t = CFRunLoopTimerCreate( + kCFAllocatorDefault, + CFAbsoluteTimeGetCurrent() + (double) delay_ms / 1000.0, 0, 0, 0, fn, + &ctx); + if (!t) + return NULL; + CFRunLoopAddTimer(fx_loop, t, kCFRunLoopDefaultMode); + CFRunLoopWakeUp(fx_loop); + return t; +} + +static void fx_deliver_terminate(fx_dev_t *d) +{ + struct { + IOServiceInterestCallback cb; + void *refcon; + } snap[FX_MAX_WATCH]; + int n = 0; + pthread_mutex_lock(&usbdev_fixture_lock); + d->terminated = true; + for (int i = 0; i < FX_MAX_WATCH; i++) { + if (d->watch[i].used && d->watch[i].cb) { + snap[n].cb = d->watch[i].cb; + snap[n].refcon = d->watch[i].refcon; + n++; + } + } + pthread_mutex_unlock(&usbdev_fixture_lock); + + /* Outside the lock, the way IOKit calls it: usbdev_interest_cb takes the + * table lock and then the entry's async_lock underneath it. + */ + for (int i = 0; i < n; i++) + snap[i].cb(snap[i].refcon, IO_OBJECT_NULL, + kIOMessageServiceIsTerminated, NULL); +} + +static void fx_terminate_cb(CFRunLoopTimerRef timer, void *info) +{ + CFRelease(timer); + fx_deliver_terminate(info); +} + +static void fx_timer_cb(CFRunLoopTimerRef timer, void *info) +{ + fx_xfer_t *x = info; + pthread_mutex_lock(&usbdev_fixture_lock); + if (!x->used || x->timer != timer) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return; + } + fx_dev_t *d = x->dev; + IOReturn result = kIOReturnSuccess; + uint32_t actual = 0; + uint8_t act = x->abort ? (uint8_t) FX_ABORTED : x->act; + switch (act) { + case FX_ABORTED: + result = kIOReturnAborted; + break; + case FX_STALL: + result = kIOUSBPipeStalled; + break; + case FX_TIMEOUT: + result = kIOUSBTransactionTimeout; + break; + case FX_NODEV: + result = kIOReturnNoDevice; + break; + case FX_ERR: + result = x->has_arg ? (IOReturn) x->arg : kIOReturnIOError; + break; + case FX_SHORT: + result = kIOReturnUnderrun; + actual = x->is_in ? fx_fill_in(d, x->buf, x->size) : x->size; + if (x->has_arg) + actual = x->arg; + break; + case FX_ZLPFAIL: + d->zlp_fail[x->ep] = true; + __attribute__((fallthrough)); + default: + actual = x->is_in ? fx_fill_in(d, x->buf, x->size) : x->size; + if (!x->is_in) + fx_store_out(d, x->buf, x->size); + if (x->has_arg) + actual = x->arg; + break; + } + fx_log_close(d, x->logidx, actual, result, x->is_in ? x->buf : NULL); + if (d->inflight_ep[x->ep]) + d->inflight_ep[x->ep]--; + IOAsyncCallback1 cb = x->cb; + void *refcon = x->refcon; + x->used = false; + x->timer = NULL; + pthread_mutex_unlock(&usbdev_fixture_lock); + CFRelease(timer); + + /* On the event thread, with no fixture lock held: exactly where + * IODispatchCalloutFromCFMessage would have run usbdev_async_cb. + */ + if (cb) + cb(refcon, result, (void *) (uintptr_t) actual); +} + +/* Hand a transfer to the fixture. + * + * Returns what the IOKit entry point returns: the async entry points never call + * back inline, because usbdev_urb_start calls them with async_lock held. + */ +static IOReturn fx_submit(fx_dev_t *d, + uint8_t ep, + bool is_in, + void *buf, + uint32_t size, + IOAsyncCallback1 cb, + void *refcon) +{ + pthread_mutex_lock(&usbdev_fixture_lock); + if (d->terminated) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNoDevice; + } + fx_step_t st; + uint32_t delay_ms = 0; + fx_next(d, ep, &st, &delay_ms); + if (st.act == FX_REFUSE) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return st.has_arg ? (IOReturn) st.arg : kIOReturnNotOpen; + } + fx_xfer_t *x = NULL; + for (int i = 0; i < FX_MAX_XFERS; i++) { + if (!fx_xfers[i].used) { + x = &fx_xfers[i]; + break; + } + } + if (!x) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNoResources; + } + memset(x, 0, sizeof(*x)); + x->used = true; + x->dev = d; + x->ep = ep; + x->is_in = is_in; + x->buf = buf; + x->size = size; + x->act = st.act; + x->has_arg = st.has_arg; + x->arg = st.arg; + x->cb = cb; + x->refcon = refcon; + unsigned conc = ++d->inflight_ep[ep]; + x->logidx = fx_log_open(d, 1, ep, is_in, size, is_in ? NULL : buf, conc); + if (st.act == FX_TERMINATE) { + /* No completion for this one, the way the board answers a terminate. */ + CFRunLoopTimerRef t = fx_arm(delay_ms, fx_terminate_cb, d); + if (!t) { + d->inflight_ep[ep]--; + x->used = false; + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNotOpen; + } + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnSuccess; + } + if (st.act != FX_NEVER && st.act != FX_WEDGE) { + x->timer = fx_arm(delay_ms, fx_timer_cb, x); + if (!x->timer) { + d->inflight_ep[ep]--; + x->used = false; + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNotOpen; + } + } + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnSuccess; +} + +/* AbortPipe / USBDeviceAbortPipeZero: asynchronous, the way IOKit's are. Every + * transfer outstanding on the endpoint is turned into kIOReturnAborted and + * completed from the event thread, which is the whole reason DISCARDURB shuts + * the endpoint's FIFO around the call. + */ +static IOReturn fx_abort(fx_dev_t *d, uint8_t ep) +{ + pthread_mutex_lock(&usbdev_fixture_lock); + if (d->aborts_gone) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNoDevice; + } + for (int i = 0; i < FX_MAX_XFERS; i++) { + fx_xfer_t *x = &fx_xfers[i]; + if (!x->used || x->dev != d || x->ep != ep) + continue; + x->abort = true; + + /* AbortPipe returns before the cancellation lands, and how long that + * takes is the host controller's business: wedge is the transfer whose + * abort outlives the engine's 2 s drain deadline, which is the only way + * in-tree to reach the orphaning path. + */ + uint32_t late = 0; + if (x->act == FX_WEDGE) + late = x->has_arg ? x->arg : FX_WEDGE_ABORT_MS; + if (x->timer && late == 0) + CFRunLoopTimerSetNextFireDate(x->timer, CFAbsoluteTimeGetCurrent()); + else if (!x->timer) + x->timer = fx_arm(late, fx_timer_cb, x); + } + if (fx_loop) + CFRunLoopWakeUp(fx_loop); + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnSuccess; +} + +/* the control plane, answered inside DeviceRequestTO */ + +#define FX_CMD_LOG 0xf0 +#define FX_CMD_SCRIPT 0xf1 +#define FX_CMD_TERMINATE 0xf2 +#define FX_CMD_RESET 0xf3 +#define FX_CMD_STATS 0xf4 + +/* wIndex flags on FX_CMD_TERMINATE (see the control plane above) */ +#define FX_TERM_PIPES 0x2u +#define FX_TERM_ABORTS 0x4u + +static bool fx_command(fx_dev_t *d, IOUSBDevRequestTO *req, IOReturn *out) +{ + if ((req->bmRequestType & 0x60) != 0x40 || req->bRequest < 0xf0) + return false; + *out = kIOReturnSuccess; + req->wLenDone = 0; + switch (req->bRequest) { + case FX_CMD_LOG: { + pthread_mutex_lock(&usbdev_fixture_lock); + unsigned first = req->wValue; + uint8_t *p = req->pData; + uint32_t room = p ? req->wLength / FX_REC_BYTES : 0; + uint32_t n = 0; + for (; first + n < d->nlog && n < room; n++) + fx_rec_serialize(&d->log[first + n], p + n * FX_REC_BYTES); + req->wLenDone = n * FX_REC_BYTES; + pthread_mutex_unlock(&usbdev_fixture_lock); + return true; + } + case FX_CMD_SCRIPT: { + char buf[512]; + + /* Refused rather than truncated. Loading the prefix installed a rule + * set the scenario did not write -- the rules past the cut simply gone + * -- while reporting the whole request as transferred, so a scenario + * that outgrew the buffer ran as a different scenario and looked like a + * pass. A script that does not fit is a mistake in the test, and this + * is what makes it say so. + */ + if (req->wLength > sizeof(buf) - 1) { + log_warn("usbdev fixture: script of %u bytes exceeds %zu", + (unsigned) req->wLength, sizeof(buf) - 1); + *out = kIOReturnBadArgument; + return true; + } + uint32_t n = req->wLength; + if (n && req->pData) + memcpy(buf, req->pData, n); + else + n = 0; + buf[n] = '\0'; + pthread_mutex_lock(&usbdev_fixture_lock); + fx_script_load(d, buf); + pthread_mutex_unlock(&usbdev_fixture_lock); + req->wLenDone = n; + return true; + } + case FX_CMD_TERMINATE: { + pthread_mutex_lock(&usbdev_fixture_lock); + if (req->wIndex & (FX_TERM_PIPES | FX_TERM_ABORTS)) { + if (req->wIndex & FX_TERM_PIPES) + d->pipes_gone = true; + if (req->wIndex & FX_TERM_ABORTS) + d->aborts_gone = true; + pthread_mutex_unlock(&usbdev_fixture_lock); + return true; + } + CFRunLoopTimerRef t = fx_arm(req->wValue, fx_terminate_cb, d); + pthread_mutex_unlock(&usbdev_fixture_lock); + if (!t) + *out = + kIOReturnNotOpen; /* no event thread yet: nothing to run on */ + return true; + } + case FX_CMD_STATS: { + /* Counters the fixture keeps about the layer above it, rather than + * about the wire. One so far: fx_release_dev's. + */ + pthread_mutex_lock(&usbdev_fixture_lock); + uint32_t v = d->dev_release_inflight; + pthread_mutex_unlock(&usbdev_fixture_lock); + if (req->pData && req->wLength >= 4) { + fx_put32(req->pData, v); + req->wLenDone = 4; + } + return true; + } + case FX_CMD_RESET: + pthread_mutex_lock(&usbdev_fixture_lock); + d->nlog = 0; + d->seq = 0; + d->t0_ms = 0; + d->stash_len = 0; + d->pipes_gone = false; + d->aborts_gone = false; + d->dev_release_inflight = 0; + memset(d->zlp_fail, 0, sizeof(d->zlp_fail)); + pthread_mutex_unlock(&usbdev_fixture_lock); + return true; + default: + *out = kIOReturnUnsupported; + return true; + } +} + +/* the two objects */ + +typedef struct { + IOUSBDeviceInterface650 *vtbl; /* first: the handle is &obj->vtbl */ + fx_dev_t *dev; +} fx_devobj_t; + +typedef struct { + IOUSBInterfaceInterface800 *vtbl; + fx_dev_t *dev; + unsigned ifnum; +} fx_ifobj_t; + +static fx_dev_t *fx_of_dev(void *self) +{ + return ((fx_devobj_t *) self)->dev; +} + +static fx_dev_t *fx_of_if(void *self) +{ + return ((fx_ifobj_t *) self)->dev; +} + +static uint8_t fx_pipe_ep(UInt8 pipeRef) +{ + if (pipeRef == 0 || pipeRef > USB_FIXTURE_LOOPBACK_NEPS) + return 0; + return usb_fixture_loopback_eps[pipeRef - 1].addr; +} + +static HRESULT fx_query(void *self, REFIID iid, LPVOID *ppv) +{ + (void) self; + (void) iid; + *ppv = NULL; + return E_NOINTERFACE; +} + +static ULONG fx_addref(void *self) +{ + (void) self; + return 1; +} + +static ULONG fx_release_dev(void *self) +{ + fx_devobj_t *o = self; + pthread_mutex_lock(&usbdev_fixture_lock); + for (int i = 0; i < FX_MAX_XFERS; i++) { + if (fx_xfers[i].used && fx_xfers[i].dev == o->dev && + fx_xfers[i].ep == 0) { + o->dev->dev_release_inflight++; + break; + } + } + pthread_mutex_unlock(&usbdev_fixture_lock); + free(self); + return 0; +} + +static ULONG fx_release_if(void *self) +{ + free(self); + return 0; +} + +static IOReturn fx_ok0(void *self) +{ + (void) self; + return kIOReturnSuccess; +} + +static void fx_source_noop(void *info) +{ + (void) info; +} + +/* A real CFRunLoopSource with nothing behind it: usbdev.c adds it to the event + * runloop and removes and releases it at teardown, and both must be genuine + * CoreFoundation calls on a genuine object. + */ +static IOReturn fx_create_source(void *self, CFRunLoopSourceRef *source) +{ + (void) self; + CFRunLoopSourceContext ctx = {0}; + ctx.perform = fx_source_noop; + CFRunLoopSourceRef s = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &ctx); + if (!s) + return kIOReturnNoMemory; + *source = s; + return kIOReturnSuccess; +} + +static IOReturn fx_dev_request_to(void *self, IOUSBDevRequestTO *req) +{ + fx_dev_t *d = fx_of_dev(self); + IOReturn cmd = kIOReturnSuccess; + if (fx_command(d, req, &cmd)) + return cmd; + pthread_mutex_lock(&usbdev_fixture_lock); + if (d->terminated) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNoDevice; + } + bool in = (req->bmRequestType & 0x80) != 0 && req->wLength != 0; + uint32_t n = in ? fx_fill_in(d, req->pData, req->wLength) : req->wLength; + if (!in) + fx_store_out(d, req->pData, req->wLength); + int idx = fx_log_open(d, 4, 0, in, req->wLength, in ? NULL : req->pData, 1); + fx_log_close(d, idx, n, kIOReturnSuccess, in ? req->pData : NULL); + req->wLenDone = n; + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnSuccess; +} + +static IOReturn fx_dev_request_async_to(void *self, + IOUSBDevRequestTO *req, + IOAsyncCallback1 cb, + void *refcon) +{ + bool in = (req->bmRequestType & 0x80) != 0 && req->wLength != 0; + return fx_submit(fx_of_dev(self), 0, in, req->pData, req->wLength, cb, + refcon); +} + +static IOReturn fx_abort_pipe_zero(void *self) +{ + return fx_abort(fx_of_dev(self), 0); +} + +/* SetConfiguration is a SET_CONFIGURATION request on the wire, so it answers a + * terminated device the way the transfers do: the seam invariant is written out + * at fx_create_iface_iterator below. + */ +static IOReturn fx_set_configuration(void *self, UInt8 cfg) +{ + fx_dev_t *d = fx_of_dev(self); + pthread_mutex_lock(&usbdev_fixture_lock); + bool gone = d->terminated; + pthread_mutex_unlock(&usbdev_fixture_lock); + if (gone) + return kIOReturnNoDevice; + return cfg == 1 ? kIOReturnSuccess : kIOReturnBadArgument; +} + +/* Invariant for the whole seam: an entry that stands for a request on the wire + * or for a question put to the device's user client answers kIOReturnNoDevice + * once the device has terminated, because that is what IOKit answers. An entry + * that stands for anything else says so here, by name, with its reason -- a + * model that keeps answering where the device would not is what lets a + * regression in the layer above land green. + * + * The entries deliberately outside it, and why: + * + * GetNumEndpoints, GetPipeProperties served from descriptor state the + * interface user client already holds, not from the wire, so termination + * is not what stops them; what stops GetPipeProperties is the pipes being + * torn down, which is its own fact and its own flag. + * AbortPipe, USBDeviceAbortPipeZero an abort after the device leaves is + * precisely what the disconnect drain issues, and IOKit lands it. Tying + * these would leave every 'never' transfer outstanding forever. + * USBInterfaceClose, USBDeviceClose the release half of a handle whose + * device has gone, which teardown has to be able to do. IOKit does not + * refuse a close against a terminated service, and usbdev.c ignores the + * result at all three call sites in any case, so a refusal would model + * nothing. + * Create{Device,Interface}AsyncEventSource local plumbing over the mach + * port rather than a call on the device, and completions are delivered + * through the source it returns. + * USBInterfaceOpen one of the three steps usbdev_fixture_open_iface stands + * for, and tied there. + * USBDeviceOpen the one entry the rule covers and the code does not, and + * not because IOKit answers differently. usbdev_lazy_device_open + * tolerates the refusal but still translates it, so a NoDevice here would + * stamp the fd from inside the same call that arms the terminate watch, and + * the late-delivery arm in usbdev_fixture_watch -- the whole reason a fresh + * fd on a departed device gets a wake at all -- would have nothing left to + * be observed by. Which of the two should originate is a question about + * that arm, not about this seam, and it is not settled here. + */ +static IOReturn fx_create_iface_iterator(void *self, + IOUSBFindInterfaceRequest *req, + io_iterator_t *iter) +{ + (void) req; + fx_dev_t *d = fx_of_dev(self); + *iter = IO_OBJECT_NULL; + pthread_mutex_lock(&usbdev_fixture_lock); + bool gone = d->terminated; + pthread_mutex_unlock(&usbdev_fixture_lock); + return gone ? kIOReturnNoDevice : kIOReturnSuccess; +} + +static IOReturn fx_get_num_endpoints(void *self, UInt8 *ne) +{ + (void) self; + *ne = USB_FIXTURE_LOOPBACK_NEPS; + return kIOReturnSuccess; +} + +static IOReturn fx_get_pipe_properties(void *self, + UInt8 pipeRef, + UInt8 *dir, + UInt8 *num, + UInt8 *type, + UInt16 *mps, + UInt8 *interval) +{ + fx_dev_t *d = fx_of_if(self); + pthread_mutex_lock(&usbdev_fixture_lock); + bool gone = d->pipes_gone; + pthread_mutex_unlock(&usbdev_fixture_lock); + if (gone) + return kIOReturnNoDevice; + if (pipeRef == 0 || pipeRef > USB_FIXTURE_LOOPBACK_NEPS) + return kIOReturnBadArgument; + const usb_fixture_ep_t *e = &usb_fixture_loopback_eps[pipeRef - 1]; + *dir = (e->addr & 0x80) ? kUSBIn : kUSBOut; + *num = e->addr & 0x0f; + *type = (e->attr & 0x03) == 0x03 ? kUSBInterrupt : kUSBBulk; + *mps = e->mps; + *interval = e->interval; + return kIOReturnSuccess; +} + +static IOReturn fx_abort_pipe(void *self, UInt8 pipeRef) +{ + return fx_abort(fx_of_if(self), fx_pipe_ep(pipeRef)); +} + +/* ClearPipeStallBothEnds sends CLEAR_FEATURE(ENDPOINT_HALT) on the wire, so it + * is inside the invariant above. + */ +static IOReturn fx_clear_stall(void *self, UInt8 pipeRef) +{ + fx_dev_t *d = fx_of_if(self); + pthread_mutex_lock(&usbdev_fixture_lock); + bool gone = d->terminated; + pthread_mutex_unlock(&usbdev_fixture_lock); + if (gone) + return kIOReturnNoDevice; + return pipeRef && pipeRef <= USB_FIXTURE_LOOPBACK_NEPS + ? kIOReturnSuccess + : kIOReturnBadArgument; +} + +/* SetAlternateInterface is a SET_INTERFACE request on the wire. */ +static IOReturn fx_set_alt(void *self, UInt8 alt) +{ + fx_dev_t *d = fx_of_if(self); + pthread_mutex_lock(&usbdev_fixture_lock); + bool gone = d->terminated; + pthread_mutex_unlock(&usbdev_fixture_lock); + if (gone) + return kIOReturnNoDevice; + return alt == 0 ? kIOReturnSuccess : kIOReturnBadArgument; +} + +static IOReturn fx_read_pipe_to(void *self, + UInt8 pipeRef, + void *buf, + UInt32 *size, + UInt32 ndt, + UInt32 ct) +{ + (void) ndt; + (void) ct; + fx_dev_t *d = fx_of_if(self); + uint8_t ep = fx_pipe_ep(pipeRef); + pthread_mutex_lock(&usbdev_fixture_lock); + if (d->terminated) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNoDevice; + } + uint32_t n = fx_fill_in(d, buf, *size); + int idx = fx_log_open(d, 2, ep, true, *size, NULL, 1); + fx_log_close(d, idx, n, kIOReturnSuccess, buf); + *size = n; + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnSuccess; +} + +/* Also the ZERO_PACKET path: usbdev_async_cb issues the terminating packet as a + * size-0 WritePipeTO from the event thread with async_lock dropped, and this is + * where that packet becomes observable. + */ +static IOReturn fx_write_pipe_to(void *self, + UInt8 pipeRef, + void *buf, + UInt32 size, + UInt32 ndt, + UInt32 ct) +{ + (void) ndt; + (void) ct; + fx_dev_t *d = fx_of_if(self); + uint8_t ep = fx_pipe_ep(pipeRef); + pthread_mutex_lock(&usbdev_fixture_lock); + if (d->terminated) { + pthread_mutex_unlock(&usbdev_fixture_lock); + return kIOReturnNoDevice; + } + IOReturn r = kIOReturnSuccess; + if (size == 0 && d->zlp_fail[ep]) { + d->zlp_fail[ep] = false; + r = kIOUSBPipeStalled; + } + int idx = fx_log_open(d, size == 0 ? 3 : 2, ep, false, size, buf, 1); + fx_log_close(d, idx, r == kIOReturnSuccess ? size : 0, r, NULL); + if (r == kIOReturnSuccess && size) + fx_store_out(d, buf, size); + pthread_mutex_unlock(&usbdev_fixture_lock); + return r; +} + +static IOReturn fx_read_pipe_async(void *self, + UInt8 pipeRef, + void *buf, + UInt32 size, + IOAsyncCallback1 cb, + void *refcon) +{ + return fx_submit(fx_of_if(self), fx_pipe_ep(pipeRef), true, buf, size, cb, + refcon); +} + +static IOReturn fx_write_pipe_async(void *self, + UInt8 pipeRef, + void *buf, + UInt32 size, + IOAsyncCallback1 cb, + void *refcon) +{ + return fx_submit(fx_of_if(self), fx_pipe_ep(pipeRef), false, buf, size, cb, + refcon); +} + +static IOReturn fx_read_pipe_async_to(void *self, + UInt8 pipeRef, + void *buf, + UInt32 size, + UInt32 ndt, + UInt32 ct, + IOAsyncCallback1 cb, + void *refcon) +{ + (void) ndt; + (void) ct; + return fx_read_pipe_async(self, pipeRef, buf, size, cb, refcon); +} + +static IOReturn fx_write_pipe_async_to(void *self, + UInt8 pipeRef, + void *buf, + UInt32 size, + UInt32 ndt, + UInt32 ct, + IOAsyncCallback1 cb, + void *refcon) +{ + (void) ndt; + (void) ct; + return fx_write_pipe_async(self, pipeRef, buf, size, cb, refcon); +} + +static IOReturn fx_control_request_async_to(void *self, + UInt8 pipeRef, + IOUSBDevRequestTO *req, + IOAsyncCallback1 cb, + void *refcon) +{ + bool in = (req->bmRequestType & 0x80) != 0 && req->wLength != 0; + return fx_submit(fx_of_if(self), fx_pipe_ep(pipeRef), in, req->pData, + req->wLength, cb, refcon); +} + +static IOUSBDeviceInterface650 fx_dev_vtbl = { + .QueryInterface = fx_query, + .AddRef = fx_addref, + .Release = fx_release_dev, + .CreateDeviceAsyncEventSource = fx_create_source, + .USBDeviceOpen = fx_ok0, + .USBDeviceClose = fx_ok0, + .SetConfiguration = fx_set_configuration, + .DeviceRequestTO = fx_dev_request_to, + .DeviceRequestAsyncTO = fx_dev_request_async_to, + .USBDeviceAbortPipeZero = fx_abort_pipe_zero, + .CreateInterfaceIterator = fx_create_iface_iterator, +}; + +static IOUSBInterfaceInterface800 fx_if_vtbl = { + .QueryInterface = fx_query, + .AddRef = fx_addref, + .Release = fx_release_if, + .CreateInterfaceAsyncEventSource = fx_create_source, + .USBInterfaceOpen = fx_ok0, + .USBInterfaceClose = fx_ok0, + .GetNumEndpoints = fx_get_num_endpoints, + .GetPipeProperties = fx_get_pipe_properties, + .SetAlternateInterface = fx_set_alt, + .AbortPipe = fx_abort_pipe, + .ClearPipeStallBothEnds = fx_clear_stall, + .ReadPipeTO = fx_read_pipe_to, + .WritePipeTO = fx_write_pipe_to, + .ReadPipeAsync = fx_read_pipe_async, + .WritePipeAsync = fx_write_pipe_async, + .ReadPipeAsyncTO = fx_read_pipe_async_to, + .WritePipeAsyncTO = fx_write_pipe_async_to, + .ControlRequestAsyncTO = fx_control_request_async_to, +}; + +/* Every entry usbdev.c reaches through one of the two handles. A vtable slot + * left NULL is a null call at runtime rather than a compile error, so the list + * is checked once instead of being discovered by a crash inside a lane. + */ +static void fx_vtable_check(void) +{ + const struct { + const void *fn; + const char *name; + } required[] = { + {(const void *) fx_dev_vtbl.Release, "Release"}, + {(const void *) fx_dev_vtbl.USBDeviceOpen, "USBDeviceOpen"}, + {(const void *) fx_dev_vtbl.USBDeviceClose, "USBDeviceClose"}, + {(const void *) fx_dev_vtbl.CreateDeviceAsyncEventSource, + "CreateDeviceAsyncEventSource"}, + {(const void *) fx_dev_vtbl.SetConfiguration, "SetConfiguration"}, + {(const void *) fx_dev_vtbl.DeviceRequestTO, "DeviceRequestTO"}, + {(const void *) fx_dev_vtbl.DeviceRequestAsyncTO, + "DeviceRequestAsyncTO"}, + {(const void *) fx_dev_vtbl.USBDeviceAbortPipeZero, + "USBDeviceAbortPipeZero"}, + {(const void *) fx_dev_vtbl.CreateInterfaceIterator, + "CreateInterfaceIterator"}, + {(const void *) fx_if_vtbl.Release, "Release"}, + {(const void *) fx_if_vtbl.USBInterfaceOpen, "USBInterfaceOpen"}, + {(const void *) fx_if_vtbl.USBInterfaceClose, "USBInterfaceClose"}, + {(const void *) fx_if_vtbl.CreateInterfaceAsyncEventSource, + "CreateInterfaceAsyncEventSource"}, + {(const void *) fx_if_vtbl.GetNumEndpoints, "GetNumEndpoints"}, + {(const void *) fx_if_vtbl.GetPipeProperties, "GetPipeProperties"}, + {(const void *) fx_if_vtbl.SetAlternateInterface, + "SetAlternateInterface"}, + {(const void *) fx_if_vtbl.AbortPipe, "AbortPipe"}, + {(const void *) fx_if_vtbl.ClearPipeStallBothEnds, + "ClearPipeStallBothEnds"}, + {(const void *) fx_if_vtbl.ReadPipeTO, "ReadPipeTO"}, + {(const void *) fx_if_vtbl.WritePipeTO, "WritePipeTO"}, + {(const void *) fx_if_vtbl.ReadPipeAsync, "ReadPipeAsync"}, + {(const void *) fx_if_vtbl.WritePipeAsync, "WritePipeAsync"}, + {(const void *) fx_if_vtbl.ReadPipeAsyncTO, "ReadPipeAsyncTO"}, + {(const void *) fx_if_vtbl.WritePipeAsyncTO, "WritePipeAsyncTO"}, + {(const void *) fx_if_vtbl.ControlRequestAsyncTO, + "ControlRequestAsyncTO"}, + }; + for (unsigned i = 0; i < sizeof(required) / sizeof(required[0]); i++) { + if (!required[i].fn) + log_warn("usbdev fixture: vtable entry %s is NULL", + required[i].name); + } +} + +/* seam */ + +void usbdev_fixture_bind_loop(CFRunLoopRef loop) +{ + if (!usbdev_fixture_loopback()) + return; + pthread_mutex_lock(&usbdev_fixture_lock); + fx_loop = loop; + pthread_mutex_unlock(&usbdev_fixture_lock); +} + +/* Resolve the one modeled device, standing it up on first use. */ +static fx_dev_t *fx_device(uint32_t location_id) +{ + if (!usbdev_fixture_loopback() || + location_id != USB_FIXTURE_LOOPBACK_LOCATION) + return NULL; + pthread_mutex_lock(&usbdev_fixture_lock); + if (!fx_dev_ready) { + fx_dev.location_id = location_id; + fx_script_load(&fx_dev, getenv("ELFUSE_USB_LOOPBACK")); + fx_dev_ready = true; + fx_vtable_check(); + } + fx_dev_t *d = &fx_dev; + pthread_mutex_unlock(&usbdev_fixture_lock); + return d; +} + +bool usbdev_fixture_has_device(uint32_t location_id, unsigned vid, unsigned pid) +{ + return fx_device(location_id) != NULL && vid == USB_FIXTURE_LOOPBACK_VID && + pid == USB_FIXTURE_LOOPBACK_PID; +} + +int64_t usbdev_fixture_open_device(uint32_t location_id, + unsigned vid, + unsigned pid, + IOUSBDeviceInterface650 ***out) +{ + fx_dev_t *d = fx_device(location_id); + if (!d || vid != USB_FIXTURE_LOOPBACK_VID || + pid != USB_FIXTURE_LOOPBACK_PID) + return -LINUX_ENODEV; + fx_devobj_t *o = calloc(1, sizeof(*o)); + if (!o) + return -LINUX_ENOMEM; + o->vtbl = &fx_dev_vtbl; + o->dev = d; + *out = &o->vtbl; + return 0; +} + +/* Inside the invariant at fx_create_iface_iterator: this stands for the + * interface service lookup, the plugin creation and USBInterfaceOpen, and IOKit + * fails all three against a service that has terminated. + * + * The answer is an errno rather than an IOReturn, so it carries no disconnect + * stamp of its own, and it does not need one: usbdev_claim_locked puts the + * device question ahead of the branch that reaches here, so a claim on a + * departed device is already -ENODEV with the fd stamped before this is called. + * What this is for is the model telling the truth, so that dropping that + * question makes the loopback lane fail instead of pass. + */ +int64_t usbdev_fixture_open_iface(uint32_t location_id, + unsigned ifnum, + IOUSBInterfaceInterface800 ***out) +{ + fx_dev_t *d = fx_device(location_id); + if (!d) + return -LINUX_ENODEV; + pthread_mutex_lock(&usbdev_fixture_lock); + bool gone = d->terminated; + pthread_mutex_unlock(&usbdev_fixture_lock); + if (gone) + return -LINUX_ENODEV; + if (ifnum != USB_FIXTURE_LOOPBACK_IFNUM) + return -LINUX_ENOENT; + fx_ifobj_t *o = calloc(1, sizeof(*o)); + if (!o) + return -LINUX_ENOMEM; + o->vtbl = &fx_if_vtbl; + o->dev = d; + o->ifnum = ifnum; + *out = &o->vtbl; + return 0; +} + +void usbdev_fixture_watch(uint32_t location_id, + IOServiceInterestCallback cb, + void *refcon) +{ + fx_dev_t *d = fx_device(location_id); + if (!d) + return; + pthread_mutex_lock(&usbdev_fixture_lock); + int free_slot = -1; + for (int i = 0; i < FX_MAX_WATCH; i++) { + if (d->watch[i].used && d->watch[i].refcon == refcon) { + free_slot = i; + break; + } + if (free_slot < 0 && !d->watch[i].used) + free_slot = i; + } + if (free_slot >= 0) { + d->watch[free_slot].used = true; + d->watch[free_slot].cb = cb; + d->watch[free_slot].refcon = refcon; + } + + /* IOServiceAddInterestNotification against a service that has already + * terminated still delivers the terminate message. Recording the watcher + * and saying nothing left the fd with no wake coming at all, so a poll or + * epoll wait on it could block for its whole timeout. + * + * Re-run the same delivery rather than calling cb from here: this runs + * under usbdev.c's per-entry lock, and usbdev_interest_cb takes + * usbdev_table_lock, which internal.h says is never held together with the + * entry lock. The event thread is where IOKit would have called it and + * where fx_deliver_terminate already calls it, holding neither. Watchers + * that were notified the first time are notified again, which costs + * nothing: the mark is idempotent. + */ + if (d->terminated && free_slot >= 0) + (void) fx_arm(0, fx_terminate_cb, d); + pthread_mutex_unlock(&usbdev_fixture_lock); +} + +void usbdev_fixture_unwatch(void *refcon) +{ + if (!usbdev_fixture_loopback()) + return; + pthread_mutex_lock(&usbdev_fixture_lock); + for (int i = 0; i < FX_MAX_WATCH; i++) { + if (fx_dev.watch[i].used && fx_dev.watch[i].refcon == refcon) + fx_dev.watch[i].used = false; + } + pthread_mutex_unlock(&usbdev_fixture_lock); +} diff --git a/src/syscall/usbdev-fixture.h b/src/syscall/usbdev-fixture.h new file mode 100644 index 00000000..3cb092b8 --- /dev/null +++ b/src/syscall/usbdev-fixture.h @@ -0,0 +1,95 @@ +/* + * The IOKit COM seam behind ELFUSE_USB_FIXTURE=loopback + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Every wire call usbdev.c makes goes through one of two opaque handles, + * IOUSBDeviceInterface650 ** and IOUSBInterfaceInterface800 **, always as + * (*h)->Method(h, ...). Handing back a fixture object whose first member is a + * vtable of the same shape substitutes for the device and changes nothing above + * it: the URB records, the per-endpoint FIFO, the completion callback, the + * readiness and disconnect maps, REAPURB and all of poll.c stay exactly the + * code that runs against hardware. The only things replaced are the calls that + * would have gone to the wire and the callbacks that would have come back from + * it. + * + * Completions are delivered from a one-shot CFRunLoopTimer on the event thread + * usbdev_loop_main runs, so usbdev_async_cb runs where + * IODispatchCalloutFromCFMessage would have run it, under no lock the fixture + * holds. The async entry points below only schedule; they never call back + * inline, because usbdev_urb_start calls them with async_lock held. + * + * What the fixture is told to do is data, not a flag: ELFUSE_USB_LOOPBACK + * carries a script of per-endpoint outcomes (see usbdev-fixture.c for the + * grammar), and the guest can replace it, read the wire log back and terminate + * the device through vendor control requests on the fixture device itself. + * + * Every entry point below answers for the fixture only. In a run that did not + * ask for it, usbdev_fixture_loopback() is false, usbdev_fixture_open_device + * refuses a location the fixture does not model, and nothing else is reachable. + * + * Two translation units define them. usbdev-fixture.c is the model, and is + * compiled only when USB_LOOPBACK_FIXTURE asks for it (mk/config.mk); + * usbdev-fixture-stub.c is the answer every other build links, and models + * nothing. usbdev.c calls these names either way and has no conditional + * compilation of its own, which is what keeps the two builds one program. + */ + +#pragma once + +#include +#include + +#include +#include +#include + +/* Whether ELFUSE_USB_FIXTURE names the loopback model. Resolved once per + * process, the shape usbdev_open_fault and fd_identity_window_delay use. + */ +bool usbdev_fixture_loopback(void); + +/* Whether the fixture models the device at this location and identity. The + * question is asked before any handle is created, because a fixture device has + * no io_service_t: usbdev.c records a flag instead of a synthetic mach port, + * and a port that is not one would eventually reach IOObjectRelease. + */ +bool usbdev_fixture_has_device(uint32_t location_id, + unsigned vid, + unsigned pid); + +/* Tell the fixture which runloop carries completions. Called once by the event + * thread as it starts; a no-op when the fixture is off. + */ +void usbdev_fixture_bind_loop(CFRunLoopRef loop); + +/* A device handle for the fixture device at location_id, or -LINUX_ENODEV when + * the fixture models no such device (which is every device but its own, so the + * other ELFUSE_USB_FIXTURE modes keep answering exactly as before). The handle + * is owned by the caller and released through its own Release entry. + */ +int64_t usbdev_fixture_open_device(uint32_t location_id, + unsigned vid, + unsigned pid, + IOUSBDeviceInterface650 ***out); + +/* An interface handle, already open: this stands in for the interface service + * lookup, the plugin creation and USBInterfaceOpen at once, because all three + * are IOKit and none of them has a per-step answer worth modeling. + * -LINUX_ENOENT for an interface number the fixture device does not carry, and + * -LINUX_ENODEV once the modeled device has terminated, because IOKit fails all + * three of those steps against a service that is gone. + */ +int64_t usbdev_fixture_open_iface(uint32_t location_id, + unsigned ifnum, + IOUSBInterfaceInterface800 ***out); + +/* Register / drop a terminate-interest callback, the + * IOServiceAddInterestNotification half of the seam. refcon is usbdev.c's + * packed slot token and identifies the registration. + */ +void usbdev_fixture_watch(uint32_t location_id, + IOServiceInterestCallback cb, + void *refcon); +void usbdev_fixture_unwatch(void *refcon); diff --git a/src/syscall/usbdev-urb.h b/src/syscall/usbdev-urb.h new file mode 100644 index 00000000..29002ab0 --- /dev/null +++ b/src/syscall/usbdev-urb.h @@ -0,0 +1,154 @@ +/* + * usbdevfs URB bookkeeping that needs no device + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The async engine in usbdev.c cannot be reached without an IOKit service + * behind the node, so the parts of it that are pure arithmetic live here + * instead: the disconnect-watch refcon, proc_do_submiturb's argument gate, the + * transferred-byte clamp, the zero-length-packet predicate and the rule that + * decides when an endpoint's queue may start its next URB. Each one is a place + * this series has already shipped a defect, and each is decided before any + * transfer, so tests/test-usbdev-urb-host.c exercises all of them on a machine + * with no board attached. + */ + +#pragma once + +#include +#include + +#include "linux-wire.h" + +/* No usbfs limit corresponds to this: Linux allocates a usb_dev_state per open. + * The fixed table is a stage-2 simplification, so exhaustion is spelled -ENOMEM + * -- a kernel-side resource shortfall -- rather than -EMFILE, which would tell + * the guest its own descriptor limit is exhausted when it is not. + */ +#define USBDEV_MAX_FDS 32 + +/* The interest refcon packs the slot index with the open's fd-table generation + * so a callback that outlives IOObjectRelease(u->notif) -- IOKit can have one + * already dispatched on the event thread -- cannot mark a reused slot: the + * generation of a later open never matches. + * + * The index field is sized from USBDEV_MAX_FDS rather than written out, because + * writing it out is what went wrong: a 32-slot table with a four-bit index + * decoded slot 16+k as slot k, so half the table never saw a disconnect and the + * other half could be marked gone while still attached. The static assertion + * below is the part that keeps the two from drifting again. + */ +#define USBDEV_WATCH_IDX_BITS 5 +#define USBDEV_WATCH_IDX_MASK ((UINT64_C(1) << USBDEV_WATCH_IDX_BITS) - 1) +#define USBDEV_WATCH_GEN_MASK \ + ((UINT64_C(1) << (60 - USBDEV_WATCH_IDX_BITS)) - 1) + +_Static_assert(USBDEV_MAX_FDS <= (1u << USBDEV_WATCH_IDX_BITS), + "watch refcon index field is narrower than the slot table"); + +static inline uintptr_t usbdev_watch_pack(unsigned idx, uint64_t generation) +{ + return (uintptr_t) (((generation & USBDEV_WATCH_GEN_MASK) + << USBDEV_WATCH_IDX_BITS) | + (idx & USBDEV_WATCH_IDX_MASK)); +} + +/* Decode a refcon. False means the token cannot name a slot, which is what the + * caller must treat as "drop this notification". + */ +static inline bool usbdev_watch_unpack(uintptr_t token, + unsigned *idx, + uint64_t *generation) +{ + unsigned i = (unsigned) (token & USBDEV_WATCH_IDX_MASK); + if (i >= USBDEV_MAX_FDS) + return false; + *idx = i; + *generation = (uint64_t) (token >> USBDEV_WATCH_IDX_BITS); + return true; +} + +/* USBFS_XFER_MAX (devio.c:140): UINT_MAX / 2 - 1000000, rejected with -EINVAL + * before any allocation is attempted, so an oversize length never reaches the + * memory budget and answers -ENOMEM. + */ +#define USBDEV_XFER_MAX (0xffffffffu / 2u - 1000000u) + +#define LINUX_URB_TYPE_ISO 0 +#define LINUX_URB_TYPE_INTERRUPT 1 +#define LINUX_URB_TYPE_CONTROL 2 +#define LINUX_URB_TYPE_BULK 3 + +#define LINUX_URB_SHORT_NOT_OK 0x01u +#define LINUX_URB_ISO_ASAP 0x02u +#define LINUX_URB_BULK_CONTINUATION 0x04u +#define LINUX_URB_NO_FSBR 0x20u +#define LINUX_URB_ZERO_PACKET 0x40u +#define LINUX_URB_NO_INTERRUPT 0x80u + +/* proc_do_submiturb's first gate (devio.c:1644-1650), in the kernel's order: + * the flags mask (ISO_ASAP is legal only on an ISO URB), then the length bound, + * then the null buffer. Everything after it in the kernel needs the endpoint, + * which is why this stops here -- resolving the endpoint before the per-type + * checks is the ordering the caller must keep, and the ordering the async path + * first got wrong while the synchronous path next door had it right. + */ +static inline int64_t usbdev_urb_arg_check(uint8_t type, + uint32_t flags, + int32_t buffer_length, + bool buffer_null) +{ + uint32_t mask = LINUX_URB_SHORT_NOT_OK | LINUX_URB_BULK_CONTINUATION | + LINUX_URB_NO_FSBR | LINUX_URB_ZERO_PACKET | + LINUX_URB_NO_INTERRUPT; + if (type == LINUX_URB_TYPE_ISO) + mask |= LINUX_URB_ISO_ASAP; + if (flags & ~mask) + return -LINUX_EINVAL; + if ((uint32_t) buffer_length >= USBDEV_XFER_MAX) + return -LINUX_EINVAL; + if (buffer_length > 0 && buffer_null) + return -LINUX_EINVAL; + return 0; +} + +/* urb->actual_length is bounded by transfer_buffer_length in every Linux HCD, + * so a guest that memcpy()s actual_length bytes out of its own buffer is + * writing within its allocation. IOKit's transferred count is a device-supplied + * number and gets the same bound here rather than being copied through. + */ +static inline uint32_t usbdev_urb_clamp_actual(uint64_t reported, + uint32_t data_len) +{ + return reported > data_len ? data_len : (uint32_t) reported; +} + +/* ZERO_PACKET: a maxpacket-multiple OUT with data gets a terminating + * zero-length packet (darwin_usb.c:3204-3218, libusb commit 2611713a). ep0 is + * excluded because the control transfer carries its own status stage. + */ +static inline bool usbdev_urb_needs_zlp(int32_t status, + bool zero_packet, + uint8_t pipe, + uint32_t data_len, + uint16_t mps) +{ + return status == 0 && zero_packet && pipe > 0 && data_len > 0 && mps != 0 && + data_len % mps == 0; +} + +/* Whether an endpoint's FIFO may hand its next URB to IOKit. AbortPipe cancels + * every transfer outstanding on a pipe, so a queue that starts the follower + * while an abort for the URB ahead of it is still in progress hands that abort + * a bystander to cancel -- measured as the discarded URB reaping success and + * its innocent successor reaping -ECONNRESET. An endpoint with an abort in + * flight, and a slot being drained wholesale, both stay shut until the abort + * that is running has returned. + */ +static inline bool usbdev_ep_may_start(bool draining, + unsigned aborting, + bool inflight) +{ + return !draining && aborting == 0 && !inflight; +} diff --git a/src/syscall/usbdev.c b/src/syscall/usbdev.c index 7529c9a0..3e2e394c 100644 --- a/src/syscall/usbdev.c +++ b/src/syscall/usbdev.c @@ -4,19 +4,30 @@ * Copyright 2026 elfuse contributors * SPDX-License-Identifier: Apache-2.0 * + * Where the line numbers below come from: devio.c is drivers/usb/core/devio.c + * at v6.15, git blob f6ce6e26e0d4, which v6.15 through v6.19 all carry + * unchanged; ioctl.c is fs/ioctl.c at v6.18; driver.c is + * drivers/usb/core/driver.c at v6.18; darwin_usb.c is libusb's + * libusb/os/darwin_usb.c at commit 2611713a. IOUSBLib.h is the IOKit framework + * header, identical in the MacOSX14.5, MacOSX15.2 and MacOSX15.5 SDKs. + * netlink.c is this tree. + * * Stage 2: a typed FD_USBDEV fd whose synchronous usbdevfs ioctls are mapped * onto IOUSBDeviceInterface650 / IOUSBInterfaceInterface800 plugin calls * (research doc D's op table). Semantics mirror drivers/usb/core/devio.c (doc A * sections A1 and A2): * * - open of any access mode succeeds; read() serves the descriptors blob - * (byte-identical to the sysfs `descriptors` attribute) at a per-open + * (byte-identical to the sysfs descriptors attribute) at a per-open * file position; SEEK_END is -EINVAL (no_seek_end_llseek). - * - every ioctl requires a writable fd: O_RDONLY fd -> -EPERM - * (devio.c:2605-2606 FMODE_WRITE gate). + * - every usbdevfs ioctl requires a writable fd: O_RDONLY fd -> -EPERM + * (devio.c:2608-2609 FMODE_WRITE gate). FIONBIO and FIOASYNC are not + * part of that surface: do_vfs_ioctl answers both for every file before + * f_op->unlocked_ioctl runs (fs/ioctl.c:507-511), so syscall/io.c answers + * them where the kernel does and they never meet this gate. * - CLAIMINTERFACE returns -EBUSY when a macOS kernel driver is bound; the * "kernel driver" test is an IORegistry child of the IOUSBHostInterface - * service in the service plane (libusb darwin_usb.c:2746-2770), and the + * service in the service plane (libusb darwin_usb.c:2761-2785), and the * claim itself is USBInterfaceOpen (kIOReturnExclusiveAccess -> -EBUSY). * - CONTROL/BULK are Linux's sync paths (do_proc_control/do_proc_bulk): * bounce buffers around DeviceRequestTO / Read|WritePipeTO, timeout in ms @@ -24,6 +35,64 @@ * kIOUSBTransactionTimeout, stall -> -EPIPE, and Linux's implicit * claim of the recipient interface (check_ctrlrecip/checkintf). * + * Stage 3 adds async URBs and poll semantics: + * + * - SUBMITURB/DISCARDURB/REAPURB/REAPURBNDELAY (doc A section A3): URB + * buffers bounce through host memory (copy-in at submit on the vCPU + * thread, copy-out at reap on the vCPU thread); IOKit completions run on + * ONE lazily-started host thread driving a CFRunLoop (the libusb darwin + * model, doc D section c) fed by CreateDeviceAsyncEventSource / + * CreateInterfaceAsyncEventSource. That thread touches only + * usbdev-owned host memory (static fd slots + malloc'd URB records), + * never guest memory, so it is safe across guest_destroy/exec + * (netlink.c:1733-1738 precedent). + * - DISCARDURB must kill exactly one URB, but IOKit's AbortPipe aborts + * every outstanding transfer on the pipe (doc D mismatch #1). So at most + * ONE URB per endpoint is in flight at IOKit at a time; later submissions + * queue inside elfuse and are started from the completion callback + * (throughput tradeoff, ep0 serialized the same way). + * - URBs get no IOKit timeout (0 = infinite): usbfs URBs never time out, + * guests cancel with DISCARDURB (kIOReturnAborted -> -ENOENT when + * discarding, -ECONNRESET otherwise, mirroring usb_kill_urb vs async + * unlink). + * - ZERO_PACKET on a maxpacket-multiple OUT issues a synchronous + * WritePipe(pipeRef, buf, 0) from the completion callback + * (darwin_usb.c:3204-3218). SHORT_NOT_OK is emulated at completion + * (-EREMOTEIO on a short IN). Both flags are honored only for their + * Linux-defined direction (devio.c:1710-1737). BULK_CONTINUATION is + * accepted but its error-cascade unlink has no IOKit counterpart. + * - poll()/select()/epoll on the fd: the backing pipe's read end raises + * host POLLIN while the fd has a completion to hand back or has been + * disconnected -- a level the pipe holds one byte for, not a byte per + * completed URB (usbdev_ready_settle_locked); poll.c remaps that to the + * guest-visible POLLOUT|POLLWRNORM (devio.c:2833-2847) via the + * usbdev_poll_* helpers below. + * - Disconnect: IOServiceAddInterestNotification (terminate message) or + * kIOReturnNoDevice/NotAttached from any op -- every op translates its + * IOKit status through usbdev_ioret_op, the synchronous transfers and the + * setup calls included, so an op that observes either of those two codes + * originates the disconnect rather than merely reporting it -- marks every + * usbfs fd open on that device disconnected, not only the one that + * noticed: the walk usbdev_remove does over udev->filelist. + * poll -> POLLERR|POLLHUP. What each ioctl answers once that mark is on + * the fd, and what it answers on an fd still carrying nothing, is the + * recorded table in tests/usbdev-ioctl-departed.tbl rather than a count + * here. + * usbdev_ioret_device_gone is deliberately narrower than the -ENODEV row + * of ioret_neg_errno, which also carries kIOReturnNotOpen: that is what + * IOKit answers for a handle nobody has opened yet, so a request arriving + * before the lazy USBDeviceOpen draws it from a device plainly still + * there. Such a request answers -ENODEV, the way Linux answers for a + * device it cannot reach, and does NOT stamp: treating that code as proof + * of departure would mark a live fd gone on its first control request. + * REAPURB hands back every URB the post-disconnect + * kill recovers BEFORE any reap answers -ENODEV, whichever reap flavor + * asked first; a non-blocking REAPURBNDELAY issues that kill's aborts, + * does not wait for them, and answers -EAGAIN until they land. + * - DISCSIGNAL stores signr/context but never delivers the signal (no + * async guest-signal injection from the event thread); URB signr is + * ignored the same way. ISO URBs are -EINVAL (doc A section A7.4, skipped). + * * Documented stage-2 deviations from Linux: * - USBDEVFS_RESET does not re-enumerate: USBDeviceReEnumerate(0) would * tear down every open plugin handle (doc D "reset" row), so RESET clears @@ -33,17 +102,28 @@ * interrupt URB; IOKit's ReadPipeTO/WritePipeTO reject interrupt pipes, * IOUSBLib.h "BadArgument if TO on interrupt pipe"). TODO(later): route * through the async path with a watchdog. - * - SUBMITURB/DISCARDURB/REAPURB* are -ENOTTY until stage 3. * - dup()/fork() of an FD_USBDEV fd are refused (-EBADF): IOKit plugin * handles are process-local and the side table is keyed by the guest fd. * TODO(later): explicit dup alias (fuse_dup_fd pattern). * - DISCONNECT/CONNECT/DISCONNECT_CLAIM cannot unbind Apple drivers without * root or the com.apple.vm.device-access entitlement, so a bound kernel * driver yields -EACCES (matching Linux's privileges-dropped answer). + * - A printer's GET_DEVICE_ID names its interface in the HIGH byte of + * wIndex and an alt setting in the low one, and check_ctrlrecip lets that + * one request through untouched: it returns 0 before the index &= 0xff + * when the recipient is 0xa1, the request is 0 and + * usb_find_alt_setting(actconfig, index >> 8, index & 0xff) has class + * USB_CLASS_PRINTER. Both control paths here read wIndex & 0xff as the + * interface number for every non-vendor interface recipient, so on a + * printer such a request implicitly claims the alt setting's number + * instead of the interface's. Demonstrating it needs a printer, so it is + * a deviations row rather than a modeled case. */ #include #include +#include +#include #include #include #include @@ -52,11 +132,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -64,8 +146,11 @@ #include "debug/log.h" #include "runtime/usb-sysfs.h" #include "syscall/internal.h" +#include "syscall/io.h" #include "syscall/linux-wire.h" #include "syscall/proc.h" +#include "syscall/usbdev-fixture.h" +#include "syscall/usbdev-urb.h" #include "syscall/usbdev.h" #include "utils.h" @@ -92,24 +177,44 @@ #define USBDEVFS_DISCONNECT_CLAIM 0x8108551bu #define USBDEVFS_GET_SPEED 0x0000551fu +/* usbdevfs requests this layer does not serve, written down so that the surface + * reaches the arm catching the undispatched and not only the dispatched arms. + * Not so that it is complete: Linux serves more that this file does not name at + * all -- CLAIM_PORT, RELEASE_PORT, FREE_STREAMS, DROP_PRIVILEGES, CONNINFO_EX, + * FORBID_SUSPEND and ALLOW_SUSPEND reach the same arm with no code here -- so + * what the codes below stand for is that arm, not a count of the surface. Each + * lands in usbdev_ioctl's default arm, and what that arm answers on a device + * that has gone is a row in tests/usbdev-ioctl-departed.tbl like any other: + * scripts/gen-usbdev-ioctl-departed.py treats a request defined here and + * dispatched nowhere as a driver for that arm, and refuses to emit while the + * arm exists with no row behind it. Adding a case for one of these turns its + * row into an ordinary dispatched row and changes nothing else. + */ +#define USBDEVFS_HUB_PORTINFO 0x80805513u +#define USBDEVFS_ALLOC_STREAMS 0x8008551cu +#define USBDEVFS_WAIT_FOR_RESUME 0x00005523u + /* Sub-codes of USBDEVFS_IOCTL (_IO('U', 22) / _IO('U', 23)). */ #define USBDEVFS_IOCTL_DISCONNECT 0x00005516 #define USBDEVFS_IOCTL_CONNECT 0x00005517 /* Capability bits (uapi/linux/usbdevice_fs.h:152-161). Every one of them - * describes the SUBMITURB/REAPURB machinery: ZERO_PACKET and BULK_CONTINUATION - * are URB flags, NO_PACKET_SIZE_LIM and BULK_SCATTER_GATHER are properties of - * how a URB is split, REAP_AFTER_DISCONNECT is about reaping. Stage 2 answers - * -ENOTTY to all of those ioctls, so it advertises none of it and reports 0; - * the async stage raises the word as it lands each one. MMAP, DROP_PRIVILEGES, - * CONNINFO_EX and SUSPEND stay clear for the same reason (doc A section A7.7). + * describes the SUBMITURB/REAPURB machinery, so the word names exactly what + * this engine honors: ZERO_PACKET, which the completion callback emits, and + * REAP_AFTER_DISCONNECT, since the reap arm answers ahead of the connected + * gate. BULK_CONTINUATION stays clear because the flag is accepted without its + * error-cascade unlink, and a guest that read the bit would rely on the + * cascade; NO_PACKET_SIZE_LIM and BULK_SCATTER_GATHER describe URB splitting + * IOKit does not expose. MMAP, DROP_PRIVILEGES, CONNINFO_EX and SUSPEND name + * ioctls this layer does not serve (doc A section A7.7). */ #define USBDEVFS_CAP_ZERO_PACKET 0x01u #define USBDEVFS_CAP_BULK_CONTINUATION 0x02u #define USBDEVFS_CAP_NO_PACKET_SIZE_LIM 0x04u #define USBDEVFS_CAP_BULK_SCATTER_GATHER 0x08u #define USBDEVFS_CAP_REAP_AFTER_DISCONNECT 0x10u -#define USBDEV_CAPS 0u +#define USBDEV_CAPS \ + (USBDEVFS_CAP_ZERO_PACKET | USBDEVFS_CAP_REAP_AFTER_DISCONNECT) #define USBDEVFS_DISCONNECT_CLAIM_IF_DRIVER 0x01u #define USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER 0x02u @@ -161,6 +266,41 @@ typedef struct { char driver[256]; } linux_usbdevfs_disconnect_claim_t; /* sizeof == 264 */ +/* struct usbdevfs_urb, LP64 (buffer at 16, usercontext at 48, sizeof 56). */ +typedef struct { + uint8_t type; + uint8_t endpoint; + uint16_t pad0; + int32_t status; + uint32_t flags; + uint32_t pad1; /* natural hole before the pointer */ + uint64_t buffer; + int32_t buffer_length; + int32_t actual_length; + int32_t start_frame; + int32_t number_of_packets; /* union with stream_id */ + int32_t error_count; + uint32_t signr; + uint64_t usercontext; +} linux_usbdevfs_urb_t; + +typedef struct { + uint32_t signr; + uint32_t pad; + uint64_t context; +} linux_usbdevfs_disconnectsignal_t; /* sizeof == 16 */ + +/* Linux poll bits (asm-generic/poll.h). POLLWRNORM differs from macOS (0x100 vs + * 0x004), so the guest-facing remap must use these, never the host's + * values. + */ +#define LINUX_POLLIN 0x0001 +#define LINUX_POLLOUT 0x0004 +#define LINUX_POLLERR 0x0008 +#define LINUX_POLLHUP 0x0010 +#define LINUX_POLLNVAL 0x0020 +#define LINUX_POLLWRNORM 0x0100 + /* do_proc_control caps wLength at PAGE_SIZE (devio.c:1182-1183). */ #define USBDEV_CTRL_MAX 4096 @@ -191,6 +331,17 @@ typedef struct { */ #define USBDEV_URB_OVERHEAD 192ull +/* do_proc_control's charge, which is not the request's length: the kernel + * bounces every control transfer through one whole page, so it books PAGE_SIZE + * + sizeof(struct urb) + sizeof(struct usb_ctrlrequest) whatever wLength says + * and gives the same amount back (devio.c:1187 and :1269). USBDEV_CTRL_MAX is + * that page (it is the same devio.c:1185 bound), the URB stands in the way the + * bulk path's does, and the setup packet is eight bytes in the kernel struct as + * on the wire. + */ +#define USBDEV_CTRL_CHARGE \ + ((uint64_t) USBDEV_CTRL_MAX + USBDEV_URB_OVERHEAD + 8ull) + /* Bytes charged against USBDEV_MEMORY_MAX, summed across every fd. */ static _Atomic uint64_t usbdev_memory_usage; @@ -222,14 +373,36 @@ static void usbdev_memory_refund(uint64_t amount) memory_order_release); } -/* side table */ +/* Ceiling on the callback-side ZERO_PACKET write. Linux has no equivalent (the + * terminating packet is part of the URB, and a URB never times out), but the + * one event thread this engine runs on carries every fd's completions, so an + * endpoint that NAKs its ZLP must not be able to stop them. + */ +#define USBDEV_ZLP_TIMEOUT_MS 1000 + +/* How long an abort has to be answered before the URB it was issued for is + * given up on and orphaned. + * + * Linux needs no such number: usb_kill_urb is synchronous, so usbdev_remove has + * every URB back before any reaper can run. IOKit's AbortPipe is not, and a + * wire that answers no abort at all would otherwise leave a disconnected fd + * owing an answer for ever. It bounds both flavors of reap, which is the whole + * of what makes -ENODEV reachable after a disconnect: the blocking one waits it + * out, the non-blocking one cannot wait and so measures it instead. + */ +#define USBDEV_DRAIN_TIMEOUT_MS 2000 -/* No usbfs limit corresponds to this: Linux allocates a usb_dev_state per open. - * The fixed table is a stage-2 simplification, so exhaustion is spelled -ENOMEM - * -- a kernel-side resource shortfall -- rather than -EMFILE, which would tell - * the guest its own descriptor limit is exhausted when it is not. +/* Milliseconds on CLOCK_MONOTONIC, for the drain deadline a non-blocking caller + * measures rather than waits out. */ -#define USBDEV_MAX_FDS 32 +static int64_t usbdev_now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t) ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +/* side table */ /* claimintf refuses ifnum >= 8 * sizeof(ps->ifclaimed) and ifclaimed is an * unsigned long (devio.c:75, :785), so the bound is 64 on every LP64 ABI elfuse @@ -242,12 +415,131 @@ static void usbdev_memory_refund(uint64_t amount) typedef struct { bool claimed; IOUSBInterfaceInterface800 **intf; + CFRunLoopSourceRef src; /* async event source, on the event runloop */ + /* Orphaned in-flight URBs still referencing intf (async_lock, like the URB + * lists): a drain timeout unlinks survivors from pending, so a later + * release rescan finds nothing -- this count is what still proves the IOKit + * handle must not be released. The late callback decrements it. + */ + int orphans; int npipes; uint8_t pipe_ep[USBDEV_MAX_PIPES]; /* pipeRef-1 -> bEndpointAddress */ uint8_t pipe_type[USBDEV_MAX_PIPES]; /* kUSBControl..kUSBInterrupt */ + uint16_t pipe_mps[USBDEV_MAX_PIPES]; /* wMaxPacketSize (ZLP check) */ } usbdev_iface_t; -typedef struct { +typedef enum { URB_QUEUED, URB_INFLIGHT, URB_COMPLETED } urb_state_t; + +struct usbdev; /* fwd */ + +/* One SUBMITURB. Guest data bounces through buf: copy-in at submit and copy-out + * at reap both happen on the vCPU thread; the completion callback (event + * thread) touches only this record and its owning static fd slot. + */ +typedef struct usbdev_urb { + /* Two lists over one record, both under async_lock. + * + * next/prev is the fd-wide list, which is the pending list while the record + * is QUEUED or INFLIGHT and the completed list afterwards; a record is on + * exactly one of the two, so next is shared and prev is pending-only. + * ep_next/ep_prev is the per-endpoint FIFO the same pending record sits in, + * keyed by ep_key. + * + * The FIFO is what makes append, unlink and restart O(1). With the fd-wide + * list alone every completion on one endpoint walked the queue of every + * other, twice -- once to unlink and once to find the next record to start + * -- and submit walked it a third time to decide whether the endpoint was + * busy, all on the one event thread holding async_lock throughout. Measured + * on the loopback fixture as the median of 31 submit-to-reap round trips on + * one endpoint against a second endpoint's pending depth: without the FIFO + * 0.017 ms at depth 0, 0.334 at 20k, 1.848 at 80k and 2.688 at 100k; with + * it 0.016, 0.025, 0.020 and 0.021. Linux is O(1) both ways + * (list_move_tail, devio.c:634). How deep that population can go is set by + * the process-wide byte budget rather than by a URB count, and the three + * added link words are what moved it: the record grows from 136 to 160 + * bytes, so 104857 zero-length records now fit in the 16 MB allowance where + * 123361 did. + */ + struct usbdev_urb *next; + struct usbdev_urb *prev; + struct usbdev_urb *ep_next, *ep_prev; + + /* Owning slot (static array, never freed). Atomic because the completion + * callback has to know which async_lock to take before it can take one, so + * this is the single field it reads outside the lock; the submitting thread + * releases it, the event thread acquires it, and ThreadSanitizer -- which + * cannot see through IODispatchCalloutFromCFMessage -- has an edge to + * follow instead of a report to file. + */ + _Atomic(struct usbdev *) u; + uint64_t userurb; /* guest pointer to struct usbdevfs_urb (reap key) */ + uint64_t data_gva; /* where IN data lands (urb buffer, +8 for control) */ + uint8_t type; /* LINUX_URB_TYPE_* */ + uint8_t ep; /* bEndpointAddress from the urb */ + uint8_t ep_key; /* per-endpoint FIFO key; 0 = default control pipe */ + uint8_t pipe; /* pipeRef; 0 = device ep0 */ + bool is_in; + uint64_t seq; /* identity a waiter can hold after the record dies */ + size_t charge; /* bytes booked against the process-wide budget */ + bool discarding; /* DISCARDURB issued: abort reports -ENOENT */ + + /* The two halves of an orphan's ownership, and the record is freed when + * both are clear. orphaned: the drain deadline gave up on this record, its + * buffer is still owned by an in-flight IOKit transfer and the late + * callback is what clears it. handback: the same record is on the completed + * list with the status usb_kill_urb leaves, and a reap (or the close that + * frees the list) is what clears it. Whichever clears last frees; an + * ordinary record has neither set and is freed by urb_free_locked. + */ + bool orphaned; + bool handback; + bool zero_packet; /* OUT + URB_ZERO_PACKET */ + bool short_not_ok; /* IN + URB_SHORT_NOT_OK */ + bool pipe_interrupt; /* pipe is interrupt-type (no *TO entry points) */ + urb_state_t state; + int32_t status; /* Linux URB status, valid once COMPLETED */ + uint32_t actual; + uint32_t data_len; /* bounce buffer length (excludes control setup) */ + uint16_t mps; /* endpoint wMaxPacketSize for the ZLP check */ + + /* Which device handle an ep0 record's orphan pin was taken against, so a + * late callback for a leaked handle cannot spend a reused slot's count -- + * the job the pointer match does for an interface's pin, which an ep0 + * record cannot do because it names no handle. A serial rather than the + * pointer because it costs nothing: it sits in the padding ahead of the + * pointers below. Meaningless unless orphaned is set. + */ + uint32_t dev_pin; + IOUSBInterfaceInterface800 **intf; /* pinned at submit; NULL for ep0 */ + uint8_t *buf; /* host bounce buffer */ + IOUSBDevRequestTO req; /* control only */ +} usbdev_urb_t; + +static inline void urb_owner_store(usbdev_urb_t *rec, struct usbdev *u) +{ + atomic_store_explicit(&rec->u, u, memory_order_release); +} + +static inline struct usbdev *urb_owner(const usbdev_urb_t *rec) +{ + return atomic_load_explicit(&rec->u, memory_order_acquire); +} + +/* The URB engine charges the same allowance the synchronous transfers charge, + * because Linux has one: usbfs_memory_usage is a single kernel-wide static + * (devio.c:143-178). Two counters of 16 MB each would let the async path queue + * a second budget behind whatever a BULK ioctl already holds. + * + * What this path adds is that the bound is a byte count, not a URB count. Both + * halves matter: a per-fd budget let a second fd queue another 16 MB, and the + * 256-record backstop this engine shipped first refused a 257th eight-byte URB + * -- 2 KB against a 16 MB budget -- which is exactly the deep ring libusb's + * async API builds for bulk streaming. The record itself is charged alongside + * its buffer so a flood of zero-length URBs is bounded too, the way Linux + * charges len + sizeof(struct urb). + */ + +typedef struct usbdev { bool used; /* slot allocated (table lock) */ bool dead; /* torn down, awaiting slot release (table lock) */ int refs; /* live usbdev_acquire pins (table lock) */ @@ -262,11 +554,22 @@ typedef struct { uint8_t *blob; /* usbfs descriptors blob (read() source) */ size_t blob_len; off_t pos; /* read()/lseek() file position */ - int pipe_wr; /* write end of the readiness pipe (stage-3 completions) */ - io_service_t service; /* retained IOUSBDevice service */ + int pipe_wr; /* write end of the readiness pipe (holds one token) */ + io_service_t service; /* retained IOUSBDevice service */ + + /* ELFUSE_USB_FIXTURE=loopback stands this device up behind the IOKit COM + * seam instead of a wire. Resolved once, at the first call that needs the + * device, and then read as a plain field: no path re-reads the environment. + * service stays IO_OBJECT_NULL for such a device rather than holding a + * synthetic port, so every IOObjectRelease and the NULL-service guard in + * usbdev_arm_disconnect_watch stay correct without a special case. + */ + bool fake; IOUSBDeviceInterface650 **dev; /* lazily created device plugin */ bool dev_open; /* USBDeviceOpen succeeded */ bool dev_open_tried; + CFRunLoopSourceRef dev_src; /* ep0 async event source (lazy) */ + io_object_t notif; /* interest notification (disconnect) */ usbdev_iface_t ifaces[USBDEV_MAX_IFACES]; /* Lock-free mirrors for cross-fd reads (SETCONFIGURATION's device-wide @@ -298,6 +601,89 @@ typedef struct { * longer taken underneath the table lock. */ pthread_mutex_t lock; + + /* --- async URB state, guarded by async_lock, which is never held across a + * blocking IOKit call: the callback-side ZLP WritePipe drops it first, and + * the cross-fd disconnect walk takes peers' beneath the table lock on the + * strength of that. The completion callback and the disconnect notification + * run on the event thread and take ONLY this lock, so a vCPU thread parked + * in a sync transfer under the entry lock cannot stall completions on other + * endpoints. Lock order: lock -> async_lock. pipe_wr may additionally be + * written under async_lock (teardown sets it to -1 under both locks before + * closing). + */ + pthread_mutex_t async_lock; + pthread_cond_t async_cv; /* completion / in-flight drain */ + usbdev_urb_t *pending_head, *pending_tail; /* QUEUED + INFLIGHT */ + usbdev_urb_t *completed_head, *completed_tail; /* reapable, FIFO */ + + /* The same pending records, split per FIFO key. The head of a key is the + * oldest record on that endpoint, so it is the INFLIGHT one when the + * endpoint has one and the next to start otherwise -- which is the whole of + * what usbdev_kick_ep_locked and SUBMITURB used to walk the fd-wide list to + * learn. One entry per bEndpointAddress plus the shared ep0 key. + */ + usbdev_urb_t *ep_head[256], *ep_tail[256]; + int inflight; /* URB_INFLIGHT count */ + int nurbs; /* live records (pending + completed) */ + size_t inflight_bytes; /* this slot's share of the global budget */ + uint64_t urb_seq; /* monotonic record id within the slot */ + + /* AbortPipe cancels every transfer outstanding on a pipe, so an endpoint + * whose own abort is still running may not start the URB behind it: + * ep_aborting counts the aborts in progress per FIFO key and draining shuts + * the whole slot for a wholesale kill. + * + * An invariant of the paths that abort deliberately, not of the slot. The + * two that raise it are DISCARDURB and every kill routed through + * usbdev_abort_urbs_locked; CLEAR_HALT and RESETEP do not, because + * ClearPipeStallBothEnds aborts the pipe as a side effect of the stall + * clear and there is no IOKit variant that does not. A queued follower can + * therefore be started behind a stall clear's abort. The guest-visible half + * of that gap is the clear-halt-collateral XFAIL in + * tests/test-usbdev-ioctl.c; closing it needs the count, the release and + * the kick DISCARDURB spells out around its own abort, so it is recorded + * here rather than half-raised. usbdev_iface_t.orphans for the device + * handle: an orphaned ep0 record references u->dev and u->dev_src and + * nothing else, so the per-interface count cannot speak for it and + * usbdev_teardown_locked had nothing left to read. A whole-slot drain + * unlinks the record from pending, so the rescan inside + * usbdev_kill_urbs_locked comes back clean and reports drained -- measured + * as close() releasing u->dev with a DeviceRequestAsyncTO still + * outstanding, no leaking-handle line printed, where the same scenario on + * an interface endpoint prints one. async_lock, like the URB lists. + */ + int dev_orphans; + + /* Names the device handle those orphans were taken against: bumped, never + * reused, every time a handle is published, so a pin recorded against a + * previous (leaked) handle of this slot cannot match. 0 is "no handle", + * which no record's pin ever carries. + */ + uint32_t dev_serial; + + uint16_t ep_aborting[256]; + bool draining; + bool disc_drained; /* the post-disconnect aborts have been issued */ + + /* When the aborts disc_drained records are given up on: they were issued + * one drain deadline before this. Meaningful only while disc_drained is + * set, and it is what lets a reap that may not wait still reach the end of + * the conversation -- see usbdev_do_reap. + */ + int64_t disc_orphan_at_ms; + + /* The cross-fd disconnect walk still owes this slot a pass. Set by the + * disconnect stamp, which two of its callers reach with async_lock held, + * and consumed by usbdev_flush_disc_peers once that lock is gone: the walk + * takes one peer's async_lock at a time and may not run underneath another. + * Atomic because it is read outside async_lock. + */ + _Atomic bool disc_peers_pending; + bool ready_token; /* the readiness pipe is holding its one token */ + bool disconnected; /* device gone; mirrored in usbdev_disc_map */ + uint32_t discsig_signr; /* DISCSIGNAL, stored but never delivered */ + uint64_t discsig_context; } usbdev_t; _Static_assert(USBDEV_MAX_IFACES <= 64, @@ -367,12 +753,87 @@ static pthread_mutex_t usbdev_table_lock = PTHREAD_MUTEX_INITIALIZER; static usbdev_t usbdev_fds[USBDEV_MAX_FDS]; static bool usbdev_ready; +/* Lock-free "this guest fd's usbdev device is gone" map for the poll/epoll + * remap helpers: they run on hot poll paths and must not queue behind an entry + * lock held across a blocking sync transfer. Set by the event thread at + * disconnect, cleared when the guest fd is bound or torn down. A stale bit on a + * reused fd number is harmless: the helpers check the fd type first, and + * binding a new usbdev fd clears it. + */ +static _Atomic uint8_t usbdev_disc_map[FD_TABLE_SIZE]; + +/* Companion map: "this guest fd has a reapable completion". usbfs poll grants + * POLLOUT|POLLWRNORM only while async_completed is non-empty (devio.c poll); + * the readiness pipe alone cannot say that, because a disconnect writes a wake + * byte too. Maintained under async_lock wherever the completed list changes; + * read lock-free on the poll paths, same discipline as usbdev_disc_map. + */ +static _Atomic uint8_t usbdev_ready_map[FD_TABLE_SIZE]; + +/* The one definition of how the two lock-free poll maps are reached. + * + * Both are published on the event thread under async_lock (disc_map at + * disconnect in mark_disconnected_locked, ready_map whenever the completed list + * changes in usbdev_ready_sync) and read with no lock by the poll/epoll remap + * helpers usbdev_fd_disconnected and usbdev_fd_reapable. The publish releases + * and the load acquires so a poller that observes the flag also observes the + * list state behind it. The URB buffer a later REAPURB copies out is not + * ordered by these: the callback fills it under async_lock and REAPURB + * re-acquires async_lock to pop it, so that release/acquire pair plus the pipe + * wake are the real payload synchronization, and the lock-free poller only ever + * consumes the boolean flag. The clears run on the owning vCPU during bind and + * teardown with no lock-free reader chasing a cleared bit, so relaxed is + * enough. Callers pass a range-checked guest fd. + */ +static inline void discmap_set(int gfd) +{ + atomic_store_explicit(&usbdev_disc_map[gfd], 1, memory_order_release); +} + +static inline void discmap_clear(int gfd) +{ + atomic_store_explicit(&usbdev_disc_map[gfd], 0, memory_order_relaxed); +} + +static inline bool discmap_load(int gfd) +{ + return atomic_load_explicit(&usbdev_disc_map[gfd], memory_order_acquire) != + 0; +} + +static inline void readymap_store(int gfd, bool ready) +{ + atomic_store_explicit(&usbdev_ready_map[gfd], ready, memory_order_release); +} + +static inline void readymap_clear(int gfd) +{ + atomic_store_explicit(&usbdev_ready_map[gfd], 0, memory_order_relaxed); +} + +static inline bool readymap_load(int gfd) +{ + return atomic_load_explicit(&usbdev_ready_map[gfd], memory_order_acquire) != + 0; +} + /* IOReturn -> -LINUX_E* (doc D table (b)) */ #ifndef kUSBHostReturnPipeStalled #define kUSBHostReturnPipeStalled 0xe0005000u #endif +/* ioret_neg_errno for a guest-visible op on a slot: the same translation, plus + * the disconnect that a device-gone answer originates. + * + * Every op goes through this rather than through the bare translation, because + * a disconnect is a fact about the device and the fd that noticed it is only + * whichever one happened to ask first. Defined next to the disconnect machinery + * below; declared here so the ops above it can use it. + */ +static int64_t usbdev_ioret_op(usbdev_t *u, IOReturn r); +static bool usbdev_ioret_device_gone(IOReturn r); + static int64_t ioret_neg_errno(IOReturn r) { switch ((uint32_t) r) { @@ -392,8 +853,10 @@ static int64_t ioret_neg_errno(IOReturn r) return -LINUX_EOVERFLOW; case kIOReturnAborted: /* A sync transfer that comes back Aborted was already on the wire - * (another thread's teardown aborted the pipe), so the dispatcher must - * not re-execute the ioctl and send it again. + * (another thread's DISCARDURB/teardown aborted the pipe), so the + * dispatcher must not re-execute the ioctl and send it again. The flag + * is thread-local; on the event thread (urb status mapping, never a + * syscall return) it is dead state and harmless. */ syscall_restart_forbid(); return -LINUX_EINTR; @@ -528,8 +991,18 @@ static io_service_t usbdev_service_for_location(uint32_t location_id) */ static int64_t usbdev_ensure_service(usbdev_t *u) { - if (u->service != IO_OBJECT_NULL) + if (u->fake || u->service != IO_OBJECT_NULL) + return 0; + + /* Fixture seam (syscall/usbdev-fixture.h), and the only place the flag is + * set. It answers for one modeled location and identity, so every other + * device -- including the other ELFUSE_USB_FIXTURE models, whose nodes have + * no service at all -- takes the registry path below unchanged. + */ + if (usbdev_fixture_has_device(u->location_id, u->vid, u->pid)) { + u->fake = true; return 0; + } io_service_t svc = usbdev_service_for_location(u->location_id); if (svc == IO_OBJECT_NULL) return -LINUX_ENODEV; @@ -543,8 +1016,27 @@ static int64_t usbdev_ensure_service(usbdev_t *u) } /* Create u->dev on first use. GetConfigurationDescriptorPtr-class calls and - * CreateInterfaceIterator need only the plugin, not USBDeviceOpen. + * CreateInterfaceIterator need only the plugin, not USBDeviceOpen. Publish a + * freshly created device handle (entry lock held). + * + * Under async_lock for the reason usbdev_claim_locked publishes an interface + * handle under it: the orphan bookkeeping reads the pair with only that lock + * held. A stale orphan count belongs to a previous (leaked) handle of this + * slot, so it starts over at zero beside the handle it counts for. */ +static void usbdev_publish_dev_locked(usbdev_t *u, + IOUSBDeviceInterface650 **dev) +{ + static _Atomic uint32_t usbdev_dev_serial; + pthread_mutex_lock(&u->async_lock); + u->dev = dev; + u->dev_orphans = 0; + u->dev_serial = + atomic_fetch_add_explicit(&usbdev_dev_serial, 1, memory_order_relaxed) + + 1; + pthread_mutex_unlock(&u->async_lock); +} + static int64_t usbdev_ensure_dev_plugin(usbdev_t *u) { if (u->dev) @@ -552,6 +1044,15 @@ static int64_t usbdev_ensure_dev_plugin(usbdev_t *u) int64_t srv = usbdev_ensure_service(u); if (srv < 0) return srv; + if (u->fake) { + IOUSBDeviceInterface650 **dev = NULL; + int64_t frc = + usbdev_fixture_open_device(u->location_id, u->vid, u->pid, &dev); + if (frc < 0) + return frc; + usbdev_publish_dev_locked(u, dev); + return 0; + } IOCFPlugInInterface **plug = NULL; SInt32 score = 0; IOReturn r = IOCreatePlugInInterfaceForService( @@ -560,7 +1061,7 @@ static int64_t usbdev_ensure_dev_plugin(usbdev_t *u) if (r != kIOReturnSuccess || !plug) { log_warn("usbdev: device plugin for %d-%d failed 0x%x", u->busnum, u->devnum, r); - return r == kIOReturnSuccess ? -LINUX_ENOMEM : ioret_neg_errno(r); + return r == kIOReturnSuccess ? -LINUX_ENOMEM : usbdev_ioret_op(u, r); } IOUSBDeviceInterface650 **dev = NULL; HRESULT hr = (*plug)->QueryInterface( @@ -568,7 +1069,7 @@ static int64_t usbdev_ensure_dev_plugin(usbdev_t *u) (*plug)->Release(plug); if (hr != S_OK || !dev) return -LINUX_ENOMEM; - u->dev = dev; + usbdev_publish_dev_locked(u, dev); return 0; } @@ -582,20 +1083,36 @@ static void usbdev_lazy_device_open(usbdev_t *u) return; u->dev_open_tried = true; IOReturn r = (*u->dev)->USBDeviceOpen(u->dev); - if (r == kIOReturnSuccess) + if (r == kIOReturnSuccess) { u->dev_open = true; - else + } else { + /* Tolerated as a failure, but still translated: a device-gone code here + * is the same news usbdev_ioret_op stamps everywhere else, and dropping + * it would leave a caller that only ever takes this path unmarked. What + * is tolerated is the refusal, not the disconnect. + */ + (void) usbdev_ioret_op(u, r); log_debug("usbdev: USBDeviceOpen %d-%d -> 0x%x (tolerated)", u->busnum, u->devnum, r); + } } -/* Retained IOUSBHostInterface service for bInterfaceNumber ifnum in the active - * configuration, or IO_OBJECT_NULL. Uses CreateInterfaceIterator so "exists" - * means exactly what claimintf's usb_ifnum_to_if means. +/* Iterator over every interface of the device in the active configuration, or + * IO_OBJECT_NULL. The one place the enumeration is opened, so the invariant + * below holds for every op that asks the device about its interfaces. + * + * Invariant: IO_OBJECT_NULL is never returned without *err saying which of the + * two answers it is. 0 means the enumeration ran, so the caller's own not-found + * errno stands; a negative value means the enumeration itself failed and is + * that failure's errno, which for a device-gone code is the whole answer -- + * Linux's usbdev_do_ioctl gate turns every one of these ops into -ENODEV once + * connected() is false. A failed enumeration is never "no interface matched": + * the question was not answered at all. */ -static io_service_t usbdev_iface_service(usbdev_t *u, unsigned ifnum) +static io_iterator_t usbdev_iface_iterator(usbdev_t *u, int64_t *err) { - if (usbdev_ensure_dev_plugin(u) < 0) + *err = usbdev_ensure_dev_plugin(u); + if (*err < 0) return IO_OBJECT_NULL; IOUSBFindInterfaceRequest fr = { .bInterfaceClass = kIOUSBFindInterfaceDontCare, @@ -604,8 +1121,27 @@ static io_service_t usbdev_iface_service(usbdev_t *u, unsigned ifnum) .bAlternateSetting = kIOUSBFindInterfaceDontCare, }; io_iterator_t it = IO_OBJECT_NULL; - if ((*u->dev)->CreateInterfaceIterator(u->dev, &fr, &it) != - kIOReturnSuccess) + IOReturn r = (*u->dev)->CreateInterfaceIterator(u->dev, &fr, &it); + if (r != kIOReturnSuccess) { + *err = usbdev_ioret_op(u, r); + return IO_OBJECT_NULL; + } + return it; +} + +/* Retained IOUSBHostInterface service for bInterfaceNumber ifnum in the active + * configuration, or IO_OBJECT_NULL. Uses the enumeration above so "exists" + * means exactly what claimintf's usb_ifnum_to_if means, and carries its err out + * unchanged: dropping it left these ops answering -ENODATA and -EINVAL (no such + * interface) on a device that was gone, and left the fd unstamped, so the ioctl + * after them answered wrongly too. + */ +static io_service_t usbdev_iface_service(usbdev_t *u, + unsigned ifnum, + int64_t *err) +{ + io_iterator_t it = usbdev_iface_iterator(u, err); + if (it == IO_OBJECT_NULL) return IO_OBJECT_NULL; io_service_t found = IO_OBJECT_NULL; io_service_t svc; @@ -621,8 +1157,35 @@ static io_service_t usbdev_iface_service(usbdev_t *u, unsigned ifnum) return found; } +/* Put the device question to IOKit, and answer only that: 0 once the + * enumeration has run, the device's own errno when it could not. + * + * Invariant at every caller below: none of them answers anything -- not an + * interface, not an interface number out of range, not a RESET that touches no + * pipe -- until the device has been asked in that call. Linux gets it from + * usbdev_do_ioctl's connected() gate, which reads device state ahead of + * proc_getdriver, proc_disconnect_claim, proc_ioctl, proc_resetdevice and + * proc_claiminterface, so the argument checks inside those live behind it; here + * neither of the two things they read is device state. u->dev is a cached + * handle usbdev_ensure_dev_plugin hands back without a call, and a claim is + * this layer's own bookkeeping, which stays true after the device leaves. Only + * a question to IOKit can come back with a device-gone code, which is what + * usbdev_ioret_op turns into the -ENODEV and the disconnect stamp the rest of + * the fd then answers from. + */ +static int64_t usbdev_ensure_dev_reachable(usbdev_t *u) +{ + int64_t err; + io_iterator_t it = usbdev_iface_iterator(u, &err); + if (err < 0) + return err; + if (it != IO_OBJECT_NULL) + IOObjectRelease(it); + return 0; +} + /* "Kernel driver bound" == the interface service has a driver child in the - * service plane (libusb darwin_usb.c:2746-2770). Fills name (class name, + * service plane (libusb darwin_usb.c:2761-2785). Fills name (class name, * truncated) when one exists. * * A user client is not a driver. IOKit publishes an @@ -662,7 +1225,7 @@ static bool usbdev_iface_driver(io_service_t ifs, char *name, size_t n) * reports for it, and flattening every failure here into -EIO renamed that as * an I/O error. -EIO stays only for a code the map has no entry for. */ -static int64_t usbdev_build_pipe_map(usbdev_iface_t *fi) +static int64_t usbdev_build_pipe_map(usbdev_t *u, usbdev_iface_t *fi) { /* Clear the whole map, not just the count: a GetPipeProperties failure at * one pipeRef of a SETINTERFACE rebuild used to leave the previous @@ -673,10 +1236,11 @@ static int64_t usbdev_build_pipe_map(usbdev_iface_t *fi) fi->npipes = 0; memset(fi->pipe_ep, 0, sizeof(fi->pipe_ep)); memset(fi->pipe_type, 0, sizeof(fi->pipe_type)); + memset(fi->pipe_mps, 0, sizeof(fi->pipe_mps)); UInt8 ne = 0; IOReturn r = (*fi->intf)->GetNumEndpoints(fi->intf, &ne); if (r != kIOReturnSuccess) { - int64_t err = ioret_neg_errno(r); + int64_t err = usbdev_ioret_op(u, r); return err < 0 ? err : -LINUX_EIO; } if (ne > USBDEV_MAX_PIPES) @@ -684,136 +1248,1299 @@ static int64_t usbdev_build_pipe_map(usbdev_iface_t *fi) for (UInt8 p = 1; p <= ne; p++) { UInt8 dir = 0, num = 0, type = 0, interval = 0; UInt16 mps = 0; - if ((*fi->intf)->GetPipeProperties(fi->intf, p, &dir, &num, &type, &mps, - &interval) != kIOReturnSuccess) + IOReturn pr = (*fi->intf)->GetPipeProperties(fi->intf, p, &dir, &num, + &type, &mps, &interval); + if (pr != kIOReturnSuccess) { + /* A device-gone code is an answer about the device and not about + * this pipeRef, so it ends the build and is reported: asking the + * next pipeRef a question already answered leaves a claim that + * succeeded with a short pipe map, whose SUBMITURB then answers + * -ENOENT (no such endpoint) where Linux answers -ENODEV. Any other + * code is about this pipeRef alone -- the map was cleared above, so + * skipping it leaves no stale address behind -- and the interface + * keeps the endpoints IOKit did describe. + */ + int64_t perr = usbdev_ioret_op(u, pr); + if (usbdev_ioret_device_gone(pr)) + return perr < 0 ? perr : -LINUX_ENODEV; continue; + } fi->pipe_ep[p - 1] = (uint8_t) (num | (dir == kUSBIn ? 0x80 : 0)); fi->pipe_type[p - 1] = type; + fi->pipe_mps[p - 1] = mps; fi->npipes = p; } return 0; } -/* claim / release (entry lock held) */ +/* async engine: event thread, URB lists, completions + * + * One host thread per elfuse process runs a CFRunLoop; per-open-device + * (CreateDeviceAsyncEventSource) and per-claimed-interface + * (CreateInterfaceAsyncEventSource) sources are added to it from vCPU threads, + * exactly libusb's darwin model (darwin_usb.c:1910-1927, 2283-2294). Chosen + * over the CreateDeviceAsyncPort + mach_msg alternative because the runloop + * also carries the IONotificationPort for disconnect interest messages with no + * extra plumbing. + * + * The thread is started lazily on the first async submit and never joined: exec + * keeps the host process (side tables survive), and after guest_destroy the + * callbacks only touch usbdev-owned host memory (static fd slots, malloc'd URB + * records, the completion pipe), never guest memory -- the copy-in/copy-out + * happens on vCPU threads (netlink.c:1733-1738 warning). + */ -static int64_t usbdev_claim_locked(usbdev_t *u, unsigned ifnum) +#ifndef kIOUSBTransactionReturned +#define kIOUSBTransactionReturned 0xe0004050u +#endif + +static pthread_mutex_t usbdev_loop_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t usbdev_loop_cv = PTHREAD_COND_INITIALIZER; +static CFRunLoopRef usbdev_loop; /* set once by the event thread */ +static bool usbdev_loop_started; +static IONotificationPortRef usbdev_notify_port; /* loop lock */ + +static void usbdev_loop_keepalive(void *info) { - if (ifnum >= USBDEV_MAX_IFACES) - return -LINUX_EINVAL; /* claimintf devio.c:786 */ - usbdev_iface_t *fi = &u->ifaces[ifnum]; - if (fi->claimed) - return 0; /* already ours */ + (void) info; +} + +static void *usbdev_loop_main(void *arg) +{ + (void) arg; - /* Ahead of the interface lookup so a device that is not there answers - * -ENODEV rather than "no such interface". + /* A permanent dummy source keeps CFRunLoopRun from returning while no + * device/interface source is attached. */ - int64_t drc = usbdev_ensure_dev_plugin(u); - if (drc < 0) - return drc; + CFRunLoopSourceContext ctx = {.perform = usbdev_loop_keepalive}; + CFRunLoopSourceRef keep = + CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &ctx); + if (keep) + CFRunLoopAddSource(CFRunLoopGetCurrent(), keep, kCFRunLoopDefaultMode); + usbdev_fixture_bind_loop(CFRunLoopGetCurrent()); + pthread_mutex_lock(&usbdev_loop_lock); + usbdev_loop = CFRunLoopGetCurrent(); + pthread_cond_broadcast(&usbdev_loop_cv); + pthread_mutex_unlock(&usbdev_loop_lock); + CFRunLoopRun(); + + /* Unreached in practice: the keepalive source pins the loop until the + * process exits. + */ + if (keep) + CFRelease(keep); + return NULL; +} - io_service_t ifs = usbdev_iface_service(u, ifnum); - if (ifs == IO_OBJECT_NULL) - return -LINUX_ENOENT; +/* Start (once) and return the event runloop; NULL only if thread creation + * failed. + */ +static CFRunLoopRef usbdev_loop_get(void) +{ + pthread_mutex_lock(&usbdev_loop_lock); + if (!usbdev_loop_started) { + pthread_t t; + pthread_attr_t at; + pthread_attr_init(&at); + pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED); + if (pthread_create(&t, &at, usbdev_loop_main, NULL) == 0) + usbdev_loop_started = true; + else + log_warn("usbdev: event thread creation failed"); + pthread_attr_destroy(&at); + } + while (usbdev_loop_started && !usbdev_loop) + pthread_cond_wait(&usbdev_loop_cv, &usbdev_loop_lock); + CFRunLoopRef l = usbdev_loop; + pthread_mutex_unlock(&usbdev_loop_lock); + return l; +} - /* Linux: a bound kernel driver makes CLAIMINTERFACE -EBUSY - * (usb_driver_claim_interface, driver.c:558). macOS arbitration is - * per-IOKit-object and dynamic rather than per-device and static, so the - * bound driver is not the question: USBInterfaceOpen answers - * kIOReturnExclusiveAccess exactly while that driver holds that interface, - * and succeeds while it is idle. Attempt the open and map what IOKit - * answers. Pre-refusing on the mere presence of a driver child refused work - * macOS grants -- the ESP32-S3's CDC data interface opens whenever nothing - * holds /dev/cu.usbmodem1101 -- and it did so from a registry snapshot that - * no live host state corresponds to. - */ - IOCFPlugInInterface **plug = NULL; - SInt32 score = 0; - IOReturn r = IOCreatePlugInInterfaceForService( - ifs, kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID, &plug, - &score); - IOObjectRelease(ifs); - if (r != kIOReturnSuccess || !plug) - return r == kIOReturnSuccess ? -LINUX_ENOMEM : ioret_neg_errno(r); - IOUSBInterfaceInterface800 **intf = NULL; - HRESULT hr = (*plug)->QueryInterface( - plug, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID800), - (LPVOID *) &intf); - (*plug)->Release(plug); - if (hr != S_OK || !intf) - return -LINUX_ENOMEM; +/* The runloop if the event thread already exists; never starts it. */ +static CFRunLoopRef usbdev_loop_current(void) +{ + pthread_mutex_lock(&usbdev_loop_lock); + CFRunLoopRef l = usbdev_loop; + pthread_mutex_unlock(&usbdev_loop_lock); + return l; +} - r = (*intf)->USBInterfaceOpen(intf); - if (r != kIOReturnSuccess) { - (*intf)->Release(intf); - int64_t e = ioret_neg_errno(r); - return e == 0 ? -LINUX_EBUSY : e; +static IONotificationPortRef usbdev_notify_get(void) +{ + CFRunLoopRef loop = usbdev_loop_get(); + if (!loop) + return NULL; + pthread_mutex_lock(&usbdev_loop_lock); + if (!usbdev_notify_port) { + usbdev_notify_port = IONotificationPortCreate(kIOMainPortDefault); + if (usbdev_notify_port) + CFRunLoopAddSource( + loop, IONotificationPortGetRunLoopSource(usbdev_notify_port), + kCFRunLoopDefaultMode); } - fi->intf = intf; - int64_t maprc = usbdev_build_pipe_map(fi); - if (maprc < 0) { - (*intf)->USBInterfaceClose(intf); - (*intf)->Release(intf); - fi->intf = NULL; - return maprc; + IONotificationPortRef p = usbdev_notify_port; + pthread_mutex_unlock(&usbdev_loop_lock); + return p; +} + +/* The two IOKit codes that mean the device is gone rather than that this + * transfer failed. Every site that sees one owes the fd a disconnect stamp, so + * the test is written once rather than at each site: the three that can see it + * are SUBMITURB's start, the completion callback, and the queued follower's + * start, and the third one did not agree with the other two. + */ +static bool usbdev_ioret_device_gone(IOReturn r) +{ + return (uint32_t) r == (uint32_t) kIOReturnNoDevice || + (uint32_t) r == (uint32_t) kIOReturnNotAttached; +} + +static void usbdev_ready_settle_locked(usbdev_t *u, int pipe_rd); + +/* Event-thread-safe disconnect stamp: async_lock only, no guest memory. Wakes + * pollers by raising the readiness level (the remap turns the wake into + * POLLERR|POLLHUP) and REAPURB waiters via the cv. Disconnect is one-way, so + * the token it takes is never given back -- which is what an unmaskable + * POLLERR|POLLHUP that stays raised needs. + */ +static void usbdev_mark_disconnected_locked(usbdev_t *u) +{ + if (!u->disconnected) { + u->disconnected = true; + + /* guest_fd is table-lock-guarded; this unlocked read races only with + * teardown, which clears the map bit again afterwards. + */ + int gfd = u->guest_fd; + if (RANGE_CHECK(gfd, 0, FD_TABLE_SIZE)) + discmap_set(gfd); + usbdev_ready_settle_locked(u, -1); + pthread_cond_broadcast(&u->async_cv); + + /* The device is gone, not this fd's view of it, so every other usbfs fd + * open on it owes the same stamp. That walk cannot run from here (this + * is called with async_lock held, and the walk takes peers'), so what + * this does is record the debt. + */ + atomic_store_explicit(&u->disc_peers_pending, true, + memory_order_release); } - fi->claimed = true; - claimed_mask_set(u, ifnum); - return 0; } -static int64_t usbdev_release_locked(usbdev_t *u, unsigned ifnum) +/* Stamp every OTHER usbfs fd open on this device, which is what a disconnect + * actually is: usbdev_remove walks udev->filelist and marks each open file + * before it wakes anything, so a second fd on the same node sees POLLERR| + * POLLHUP and -ENODEV without having had to touch the device itself. Here the + * stamp used to be written only where it was noticed -- the terminate watch is + * armed from SUBMITURB alone, and the three other sites are the async engine's + * -- so an fd that never submitted an async URB was told nothing: measured, + * poll revents 0x0000 and GET_CAPABILITIES 0 on a device whose sibling fd had + * already been given 0x0018 and -ENODEV. + * + * usbdev_table_lock, held by the caller, is what stops a slot from being torn + * down and reused between the devkey match and the stamp; the peers' async + * locks are taken one at a time beneath it and never two at once, so this adds + * no order beyond the table -> async nesting usbdev_interest_cb already has. + * The entry locks are not taken at all, for the reason usbdev_claimed_elsewhere + * gives: one can be held across a whole transfer timeout. + */ +static void usbdev_mark_key_disconnected_tlocked(uint64_t key, + const usbdev_t *skip) { - if (ifnum >= USBDEV_MAX_IFACES) - return -LINUX_EINVAL; - usbdev_iface_t *fi = &u->ifaces[ifnum]; - if (!fi->claimed) { - /* releaseintf checks usb_ifnum_to_if first: a nonexistent interface is - * -ENOENT, an existing unclaimed one -EINVAL (devio.c:815-833). + if (!key) + return; + for (int i = 0; i < USBDEV_MAX_FDS; i++) { + usbdev_t *o = &usbdev_fds[i]; + if (o == skip || !o->used || o->dead || devkey_load(o) != key) + continue; + pthread_mutex_lock(&o->async_lock); + usbdev_mark_disconnected_locked(o); + + /* Stamped by this walk, so it owes no walk of its own: the device is + * covered, and leaving the debt set would have every peer re-run it. */ - int64_t drc = usbdev_ensure_dev_plugin(u); - if (drc < 0) - return drc; - io_service_t ifs = usbdev_iface_service(u, ifnum); - if (ifs == IO_OBJECT_NULL) - return -LINUX_ENOENT; - IOObjectRelease(ifs); - return -LINUX_EINVAL; + atomic_store_explicit(&o->disc_peers_pending, false, + memory_order_relaxed); + pthread_mutex_unlock(&o->async_lock); } - (*fi->intf)->USBInterfaceClose(fi->intf); - (*fi->intf)->Release(fi->intf); - fi->intf = NULL; - fi->claimed = false; - claimed_mask_clear(u, ifnum); - fi->npipes = 0; - return 0; } -/* usbdev_ep_owner_iface's two failures, kept apart because Linux answers them - * differently: an endpoint no altsetting carries is -ENOENT (findintfep's own - * return), while one whose owning interface number is past the claim bitmap is - * -EINVAL (checkintf, devio.c:842). +/* Keyed by the slot, for every caller that still has a live one. */ +static void usbdev_mark_peers_disconnected_tlocked(usbdev_t *u) +{ + atomic_store_explicit(&u->disc_peers_pending, false, memory_order_relaxed); + usbdev_mark_key_disconnected_tlocked(devkey_load(u), u); +} + +/* Pay off whatever the stamp recorded, from a caller holding neither async_lock + * nor the table lock. Cheap enough to sit on every ioctl's way out + * (usbdev_release): one relaxed exchange when there is nothing to do. */ -#define USBDEV_EP_OWNER_NONE (-1) -#define USBDEV_EP_OWNER_OUT_OF_RANGE (-2) +static void usbdev_flush_disc_peers(usbdev_t *u) +{ + if (!atomic_exchange_explicit(&u->disc_peers_pending, false, + memory_order_acq_rel)) + return; + pthread_mutex_lock(&usbdev_table_lock); + usbdev_mark_peers_disconnected_tlocked(u); + pthread_mutex_unlock(&usbdev_table_lock); +} -/* findintfep (devio.c:853-876): which interface of the active config carries - * bEndpointAddress ep, searching every altsetting. Parsed from the descriptors - * blob. +/* Take a slot's unpaid peer-walk debt, and the devkey the walk needs, off it + * before teardown retires both. * - * Returns USBDEV_EP_OWNER_NONE when not found, or USBDEV_EP_OWNER_OUT_OF_RANGE - * when the endpoint is carried by an interface number this layer cannot - * represent. Never returns a number ifaces[] does not hold. + * usbdev_flush_disc_peers cannot serve a closing fd. It needs the table lock, + * which never nests under an entry lock, so it can only run after the entry + * lock is dropped -- by which time usbdev_teardown_locked has cleared + * disc_peers_pending and called devkey_retire, so the flush read a false debt + * and, had it read a true one, an empty key. Both flushes on the close paths + * were therefore dead by construction. Reading the pair out first, while the + * slot is still whole, is what makes them do the work their comments claim: the + * debt is answered from the key rather than from the slot, which by then may + * already have been reused for somebody else's device. */ -static int usbdev_ep_owner_iface(const usbdev_t *u, uint8_t ep) +static uint64_t usbdev_take_disc_debt(usbdev_t *u) { - const uint8_t *b = u->blob; - size_t len = u->blob_len; - size_t off = 18; - while (off + 9 <= len && b[off + 1] == 0x02 /* CONFIG */) { - size_t total = (size_t) b[off + 2] | ((size_t) b[off + 3] << 8); - if (total < 9 || off + total > len) - break; - bool active = b[off + 5] == (uint8_t) u->cfg_value; + if (!atomic_exchange_explicit(&u->disc_peers_pending, false, + memory_order_acq_rel)) + return 0; + return devkey_load(u); +} + +/* Pay what usbdev_take_disc_debt took, from a caller holding no lock at all. */ +static void usbdev_pay_disc_debt(uint64_t key, const usbdev_t *skip) +{ + if (!key) + return; + pthread_mutex_lock(&usbdev_table_lock); + usbdev_mark_key_disconnected_tlocked(key, skip); + pthread_mutex_unlock(&usbdev_table_lock); +} + +static void usbdev_mark_disconnected(usbdev_t *u) +{ + pthread_mutex_lock(&u->async_lock); + usbdev_mark_disconnected_locked(u); + pthread_mutex_unlock(&u->async_lock); +} + +/* Declared above ioret_neg_errno. async_lock must not be held (this takes it); + * the entry lock may be, which is the state every op call site is in. The peer + * walk the stamp records is paid on the way out, in usbdev_release. + */ +static int64_t usbdev_ioret_op(usbdev_t *u, IOReturn r) +{ + if (usbdev_ioret_device_gone(r)) + usbdev_mark_disconnected(u); + return ioret_neg_errno(r); +} + +/* Refcon layout and the reason for it: usbdev-urb.h. */ +static void *usbdev_watch_token(const usbdev_t *u) +{ + return (void *) usbdev_watch_pack((unsigned) (u - usbdev_fds), + u->generation); +} + +static void usbdev_interest_cb(void *refcon, + io_service_t service, + natural_t msg, + void *arg) +{ + (void) service; + (void) arg; + if (msg != kIOMessageServiceIsTerminated) + return; + + /* Re-validate the token before marking: used/generation are table-lock + * fields, and holding the table lock here also orders this against a + * concurrent teardown + slot reuse (table -> async is the documented order, + * so the nested mark is safe). + */ + unsigned idx; + uint64_t gen; + if (!usbdev_watch_unpack((uintptr_t) refcon, &idx, &gen)) + return; + usbdev_t *u = &usbdev_fds[idx]; + pthread_mutex_lock(&usbdev_table_lock); + + if (u->used && (u->generation & USBDEV_WATCH_GEN_MASK) == gen) { + /* dead is what the peer walk already tests beside used, and here it + * guards the stamp alone. A slot whose fd has closed still holds its + * generation until the table releases it, so a terminate arriving in + * that window stamps a slot that no longer answers for anything. What + * outlives the slot is not the flag but the map: the mark writes + * usbdev_disc_map under this slot's guest fd number, and + * usbdev_teardown_locked has already cleared both by the time the mark + * lands, so the bit stays set under an fd number that is free for + * reuse. Measured on the loopback fixture: a close blocked in the 2 s + * drain, a terminate delivered underneath it, and the next open handed + * the same guest fd -- a different node, with no device of its own -- + * polled POLLERR|POLLHUP (0x0018) 3/3, where the same run with the + * terminate armed past the drain polled 0x0000 3/3. The errno is not + * the tell: CLAIMINTERFACE answers -ENODEV in both runs, because the + * node the reused fd number landed on carries no IOKit device at all. + */ + if (!u->dead) + usbdev_mark_disconnected(u); + + /* The walk is not guarded. It stamps the device's OTHER fds, which are + * live whatever this slot's state is, and a closing fd is the last one + * that can tell them -- a sync-only peer on the same node arms no watch + * of its own, so the departure reaches it by this route or not at all. + * Skipping it measured 0x0000 3/3 on such a peer for a device that was + * gone. devkey_retire runs inside the teardown the drain above has not + * reached yet, so the key the walk needs is still on the slot; once the + * teardown has run the key reads 0 and the walk returns at once. + * + * In place rather than through usbdev_flush_disc_peers: the table lock + * this needs is already held here and is not recursive. + */ + usbdev_mark_peers_disconnected_tlocked(u); + } + pthread_mutex_unlock(&usbdev_table_lock); +} + +/* Register the terminate-interest notification (entry lock held). Best effort: + * kIOReturnNoDevice detection on ops is the fallback. + */ +static void usbdev_arm_disconnect_watch(usbdev_t *u) +{ + if (u->notif != IO_OBJECT_NULL) + return; + if (u->fake) { + /* Same callback, same packed token: what changes is who posts the + * terminate message. + */ + usbdev_fixture_watch(u->location_id, usbdev_interest_cb, + usbdev_watch_token(u)); + return; + } + if (u->service == IO_OBJECT_NULL) + return; + IONotificationPortRef port = usbdev_notify_get(); + if (!port) + return; + if (IOServiceAddInterestNotification( + port, u->service, kIOGeneralInterest, usbdev_interest_cb, + usbdev_watch_token(u), &u->notif) != kIOReturnSuccess) + u->notif = IO_OBJECT_NULL; +} + +/* Create + attach the ep0 async event source (entry lock held). */ +static int64_t usbdev_ensure_dev_async(usbdev_t *u) +{ + int64_t rc = usbdev_ensure_dev_plugin(u); + if (rc < 0) + return rc; + usbdev_lazy_device_open(u); + if (u->dev_src) + return 0; + CFRunLoopRef loop = usbdev_loop_get(); + if (!loop) + return -LINUX_ENOMEM; + CFRunLoopSourceRef src = NULL; + IOReturn r = (*u->dev)->CreateDeviceAsyncEventSource(u->dev, &src); + if (r != kIOReturnSuccess || !src) + return r == kIOReturnSuccess ? -LINUX_ENOMEM : usbdev_ioret_op(u, r); + CFRunLoopAddSource(loop, src, kCFRunLoopDefaultMode); + u->dev_src = src; + usbdev_arm_disconnect_watch(u); + return 0; +} + +/* Create + attach the interface async event source (entry lock held; fi is + * claimed). + */ +static int64_t usbdev_ensure_iface_async(usbdev_t *u, usbdev_iface_t *fi) +{ + if (fi->src) + return 0; + CFRunLoopRef loop = usbdev_loop_get(); + if (!loop) + return -LINUX_ENOMEM; + CFRunLoopSourceRef src = NULL; + IOReturn r = (*fi->intf)->CreateInterfaceAsyncEventSource(fi->intf, &src); + if (r != kIOReturnSuccess || !src) + return r == kIOReturnSuccess ? -LINUX_ENOMEM : usbdev_ioret_op(u, r); + CFRunLoopAddSource(loop, src, kCFRunLoopDefaultMode); + fi->src = src; + usbdev_arm_disconnect_watch(u); + return 0; +} + +/* URB lists (async_lock held) */ + +static void urb_list_append(usbdev_urb_t **head, + usbdev_urb_t **tail, + usbdev_urb_t *rec) +{ + rec->next = NULL; + if (*tail) + (*tail)->next = rec; + else + *head = rec; + *tail = rec; +} + +/* Append to the pending list and to its endpoint's FIFO, both at the tail, so + * each list is in submission order (async_lock held). + */ +static void urb_pending_append(usbdev_t *u, usbdev_urb_t *rec) +{ + rec->next = NULL; + rec->prev = u->pending_tail; + if (u->pending_tail) + u->pending_tail->next = rec; + else + u->pending_head = rec; + u->pending_tail = rec; + + uint8_t key = rec->ep_key; + rec->ep_next = NULL; + rec->ep_prev = u->ep_tail[key]; + if (u->ep_tail[key]) + u->ep_tail[key]->ep_next = rec; + else + u->ep_head[key] = rec; + u->ep_tail[key] = rec; +} + +/* Take a record off both pending lists. A record that is on neither is left + * alone, which the head test is what decides: every caller here holds a record + * it took from the pending list, and the test costs one compare (async_lock + * held). + */ +static void urb_pending_unlink(usbdev_t *u, usbdev_urb_t *rec) +{ + if (!rec->prev && u->pending_head != rec) + return; + if (rec->prev) + rec->prev->next = rec->next; + else + u->pending_head = rec->next; + if (rec->next) + rec->next->prev = rec->prev; + else + u->pending_tail = rec->prev; + rec->next = rec->prev = NULL; + + uint8_t key = rec->ep_key; + if (rec->ep_prev) + rec->ep_prev->ep_next = rec->ep_next; + else if (u->ep_head[key] == rec) + u->ep_head[key] = rec->ep_next; + if (rec->ep_next) + rec->ep_next->ep_prev = rec->ep_prev; + else if (u->ep_tail[key] == rec) + u->ep_tail[key] = rec->ep_prev; + rec->ep_next = rec->ep_prev = NULL; +} + +static void urb_free_locked(usbdev_t *u, usbdev_urb_t *rec) +{ + u->nurbs--; + u->inflight_bytes -= rec->charge; + usbdev_memory_refund(rec->charge); + free(rec->buf); + free(rec); +} + +/* Restate the fd's readiness -- the lock-free reapable map and the readiness + * pipe -- after anything that can have moved either term (async_lock held). + * + * INVARIANT, restored before async_lock is dropped and true of every live + * usbdevfs fd: the readiness pipe holds exactly one byte while completed_head + * is non-empty or the fd is disconnected, and no bytes otherwise. It is a + * level, not a running count of completions. + * + * A byte per completion cannot carry that. There is no URB-count cap, only the + * process-wide byte budget, and a zero-length URB costs just + * sizeof(usbdev_urb_t) against it, so far more records fit than the pipe has + * room for bytes; a nonblocking write whose byte does not fit leaves its record + * reapable with nothing readable behind it, and poll, select and epoll then + * answer 0 for an fd that would reap at once. One token is never the byte that + * does not fit, and needs no room it has not already taken. + * + * Only a reap lowers the level, and only it holds the pipe's read end (the fd + * table owns it); the callers that can only raise it -- a completion, a + * disconnect -- pass -1. Teardown drops the pipe rather than draining it, and + * clears the flag with pipe_wr. + */ +static void usbdev_ready_settle_locked(usbdev_t *u, int pipe_rd) +{ + /* The unlocked guest_fd read races only teardown, which re-clears the bit, + * the usbdev_mark_disconnected_locked pattern. + */ + int gfd = u->guest_fd; + if (RANGE_CHECK(gfd, 0, FD_TABLE_SIZE)) + readymap_store(gfd, u->completed_head != NULL); + + bool want = u->completed_head != NULL || u->disconnected; + if (want == u->ready_token) + return; + if (want) { + char b = 0; + if (u->pipe_wr >= 0 && write(u->pipe_wr, &b, 1) == 1) + u->ready_token = true; + } else if (pipe_rd >= 0) { + /* Never blocks: the token is known present, and both ends are + * nonblocking anyway. + */ + char b; + if (read(pipe_rd, &b, 1) == 1) + u->ready_token = false; + } +} + +/* Move rec to the completed list and raise readiness (async_lock held). */ +static void urb_complete_locked(usbdev_t *u, usbdev_urb_t *rec, int32_t status) +{ + rec->status = status; + rec->state = URB_COMPLETED; + urb_list_append(&u->completed_head, &u->completed_tail, rec); + usbdev_ready_settle_locked(u, -1); +} + +/* kIOReturn -> Linux URB status (doc D table (b); differs from the sync ioctl + * map in the Aborted row: DISCARDURB = usb_kill_urb = -ENOENT, any other abort + * = async unlink = -ECONNRESET). + */ +static int32_t usbdev_urb_status(const usbdev_urb_t *rec, IOReturn r) +{ + switch ((uint32_t) r) { + case kIOReturnSuccess: + case kIOReturnUnderrun: /* short transfer == success */ + return 0; + case kIOReturnAborted: + case kIOUSBTransactionReturned: + return rec->discarding ? -LINUX_ENOENT : -LINUX_ECONNRESET; + case kIOReturnNoDevice: + case kIOReturnNotOpen: + case kIOReturnNotAttached: + return -LINUX_ENODEV; + default: + return (int32_t) ioret_neg_errno(r); + } +} + +static void usbdev_async_cb(void *refcon, IOReturn result, void *arg0); + +/* Hand rec to IOKit. Called with async_lock held: the async entry points do not + * block, and their callbacks arrive later on the event thread. + */ +static IOReturn usbdev_urb_start(usbdev_urb_t *rec) +{ + usbdev_t *u = urb_owner(rec); + if (rec->type == LINUX_URB_TYPE_CONTROL) { + rec->req.pData = rec->buf; + if (rec->pipe == 0) + return (*u->dev)->DeviceRequestAsyncTO(u->dev, &rec->req, + usbdev_async_cb, rec); + return (*rec->intf) + ->ControlRequestAsyncTO(rec->intf, rec->pipe, &rec->req, + usbdev_async_cb, rec); + } + + /* Interrupt pipes reject the *TO entry points (IOUSBLib.h: BadArgument); + * bulk gets the TO variants with 0 = infinite, matching usbfs URBs that + * never time out. A BULK URB on an interrupt endpoint lands here with + * pipe_interrupt set, i.e. Linux's silent bulk->interrupt conversion + * (devio.c:1718-1721). + */ + if (rec->pipe_interrupt) { + if (rec->is_in) + return (*rec->intf) + ->ReadPipeAsync(rec->intf, rec->pipe, rec->buf, rec->data_len, + usbdev_async_cb, rec); + return (*rec->intf) + ->WritePipeAsync(rec->intf, rec->pipe, rec->buf, rec->data_len, + usbdev_async_cb, rec); + } + if (rec->is_in) + return (*rec->intf) + ->ReadPipeAsyncTO(rec->intf, rec->pipe, rec->buf, rec->data_len, 0, + 0, usbdev_async_cb, rec); + return (*rec->intf) + ->WritePipeAsyncTO(rec->intf, rec->pipe, rec->buf, rec->data_len, 0, 0, + usbdev_async_cb, rec); +} + +/* Restart the FIFO on one endpoint key: submit the oldest QUEUED record if the + * endpoint may start one (usbdev_ep_may_start); locally fail records IOKit + * refuses (async_lock held). + */ +static void usbdev_kick_ep_locked(usbdev_t *u, uint8_t key) +{ + for (;;) { + /* The FIFO's head is the oldest record on this endpoint, so it is the + * one in flight when the endpoint has one and the next to start + * otherwise. Reading it is what replaced a walk of every other + * endpoint's queue. + */ + usbdev_urb_t *head = u->ep_head[key]; + bool inflight = head && head->state == URB_INFLIGHT; + usbdev_urb_t *next = inflight ? NULL : head; + if (!usbdev_ep_may_start(u->draining, u->ep_aborting[key], inflight)) + return; + if (!next) + return; + IOReturn ir = usbdev_urb_start(next); + if (ir == kIOReturnSuccess) { + next->state = URB_INFLIGHT; + u->inflight++; + return; + } + + /* A follower that starts after the device has gone is the same event + * SUBMITURB and the completion callback stamp, and it used to be the + * one site that only wrote the URB's -ENODEV: pollers then saw no + * POLLERR|POLLHUP and a later ioctl passed the disconnected gate. + */ + if (usbdev_ioret_device_gone(ir)) + usbdev_mark_disconnected_locked(u); + + /* The completion map, not the syscall map: a start that comes back + * Aborted is a canceled URB, and Linux writes -ECONNRESET/-ENOENT into + * urb->status for that. ioret_neg_errno's -EINTR is a syscall return + * value the kernel never puts in a URB. + */ + int32_t st = usbdev_urb_status(next, ir); + urb_pending_unlink(u, next); + urb_complete_locked(u, next, st ? st : -LINUX_EPROTO); + } +} + +/* Give the handles an orphan pins back: the late callback has arrived, so IOKit + * owns neither the buffer nor the interface any more (async_lock held). + * + * Whether the record itself may be freed is the other half of the ownership -- + * a handed-back orphan is still on the completed list waiting for a reap -- so + * this only clears orphaned and leaves that to the caller. + */ +static void urb_orphan_retire_locked(usbdev_t *u, usbdev_urb_t *rec) +{ + /* Un-pin the owning handle: the interface's for a record that named one, + * the device's for an ep0 record, which names none. The pointer match + * cannot hit a reused slot's interface: an orphan's handle is leaked, never + * freed, so no later claim can be allocated at the same address. + */ + if (rec->intf) { + for (int i = 0; i < USBDEV_MAX_IFACES; i++) { + if (u->ifaces[i].intf == rec->intf && u->ifaces[i].orphans > 0) { + u->ifaces[i].orphans--; + break; + } + } + } else if (rec->dev_pin == u->dev_serial && u->dev_orphans > 0) { + u->dev_orphans--; + } + rec->orphaned = false; +} + +/* Free an orphaned record: a drain timeout already unlinked it and settled the + * slot's counters, and the slot may since have been reused by a new open, so + * this frees only what the record owns -- no counters, no disconnect map, no + * readiness token (async_lock held). + * + * Called only once both halves of the ownership are clear, which is why it + * takes no slot: there may no longer be one this record belongs to. + */ +static void urb_free_orphan_locked(usbdev_urb_t *rec) +{ + /* The process-wide charge outlives the drain deadline because the memory + * does: it is given back here, with the buffer it was taken for. A charge + * that never comes back is a transfer whose callback never arrived, which + * is the honest answer rather than a leak. + */ + usbdev_memory_refund(rec->charge); + free(rec->buf); + free(rec); +} + +/* The late callback's end of an orphan: retire the pins, and free the record + * unless it is still on the completed list owing the guest an answer + * (async_lock held). + */ +static void urb_orphan_callback_locked(usbdev_t *u, usbdev_urb_t *rec) +{ + urb_orphan_retire_locked(u, rec); + if (!rec->handback) + urb_free_orphan_locked(rec); +} + +/* A reap or a close taking a handed-back orphan off the completed list: free it + * unless IOKit still owes its callback (async_lock held). + */ +static void urb_handback_taken_locked(usbdev_urb_t *rec) +{ + rec->handback = false; + if (!rec->orphaned) + urb_free_orphan_locked(rec); +} + +/* IOKit completion (event thread). arg0 carries the transferred byte count for + * pipe reads/writes and wLenDone for device requests. + */ +static void usbdev_async_cb(void *refcon, IOReturn result, void *arg0) +{ + usbdev_urb_t *rec = refcon; + usbdev_t *u = urb_owner(rec); + pthread_mutex_lock(&u->async_lock); + if (rec->orphaned) { + urb_orphan_callback_locked(u, rec); + pthread_mutex_unlock(&u->async_lock); + return; + } + if (usbdev_ioret_device_gone(result)) + usbdev_mark_disconnected_locked(u); + + /* The transferred count is a device-supplied number; Linux's HCDs bound + * urb->actual_length by transfer_buffer_length and so does this. + */ + rec->actual = + usbdev_urb_clamp_actual((uint64_t) (uintptr_t) arg0, rec->data_len); + int32_t st = usbdev_urb_status(rec, result); + if (st == 0 && rec->short_not_ok && rec->actual < rec->data_len) + st = -LINUX_EREMOTEIO; /* URB_SHORT_NOT_OK, devio.c error-codes */ + + /* ZLP: a maxpacket-multiple OUT gets its terminating zero-length packet as + * a separate WritePipe from the callback (darwin_usb.c:3204-3218). Two + * things the first cut of this got wrong, both of them the whole process's + * problem rather than this URB's: the untimed entry point on an endpoint + * that NAKs never returns, and there is one event thread for every usbdevfs + * fd in the process, so a wedge there stops all completions and every + * SUBMITURB/DISCARDURB/REAPURB waiting on this async_lock. It is issued + * with the lock dropped and with a bounded timeout, and a ZLP that fails + * lands in the URB's status the way it does on Linux, where the terminating + * packet is part of the URB rather than a second transfer. + */ + if (usbdev_urb_needs_zlp(st, rec->zero_packet, rec->pipe, rec->data_len, + rec->mps)) { + IOUSBInterfaceInterface800 **intf = rec->intf; + uint8_t pipe = rec->pipe; + uint8_t *buf = rec->buf; + pthread_mutex_unlock(&u->async_lock); + IOReturn zr = (*intf)->WritePipeTO( + intf, pipe, buf, 0, USBDEV_ZLP_TIMEOUT_MS, USBDEV_ZLP_TIMEOUT_MS); + pthread_mutex_lock(&u->async_lock); + if (rec->orphaned) { + urb_orphan_callback_locked(u, rec); + pthread_mutex_unlock(&u->async_lock); + usbdev_flush_disc_peers(u); + return; + } + if (zr != kIOReturnSuccess) { + st = usbdev_urb_status(rec, zr); + if (st == 0) + st = -LINUX_EPROTO; + } + } + urb_pending_unlink(u, rec); + u->inflight--; + uint8_t key = rec->ep_key; + urb_complete_locked(u, rec, st); + usbdev_kick_ep_locked(u, key); + pthread_cond_broadcast(&u->async_cv); + pthread_mutex_unlock(&u->async_lock); + + /* Both stamps this callback can leave -- its own device-gone test and the + * one usbdev_kick_ep_locked writes when a follower's start finds the device + * gone -- run with async_lock held, so the cross-fd walk they owe is paid + * here. + */ + usbdev_flush_disc_peers(u); +} + +/* The abort half of a kill: retire every matching QUEUED record as killed + * (-ENOENT, usb_kill_urb) and issue the AbortPipe calls the INFLIGHT ones need, + * without waiting for the callbacks those provoke. Answers whether anything was + * left in flight for a drain to wait on. Entry lock held. + * + * Split out from usbdev_kill_urbs_locked because the aborts are what recovers + * the URBs and the wait is only how a caller learns that they landed, and one + * caller -- a non-blocking REAPURBNDELAY -- may issue the first and must not + * perform the second. + */ +static bool usbdev_abort_urbs_locked(usbdev_t *u, + IOUSBInterfaceInterface800 **intf) +{ + pthread_mutex_lock(&u->async_lock); + + /* Shut every endpoint's FIFO for the whole abort-and-drain: a completion + * arriving mid-kill must not start a queued URB behind the AbortPipe that + * is still running for the one ahead of it. + */ + u->draining = true; + usbdev_urb_t *r = u->pending_head; + bool any_inflight = false; + while (r) { + usbdev_urb_t *nx = r->next; + bool match = intf == NULL || r->intf == intf; + if (match && r->state == URB_QUEUED) { + urb_pending_unlink(u, r); + urb_complete_locked(u, r, -LINUX_ENOENT); + } else if (match && r->state == URB_INFLIGHT) { + r->discarding = true; + any_inflight = true; + } + r = nx; + } + pthread_mutex_unlock(&u->async_lock); + + if (any_inflight) { + if (intf == NULL && u->dev && u->dev_src) + (void) (*u->dev)->USBDeviceAbortPipeZero(u->dev); + for (int i = 0; i < USBDEV_MAX_IFACES; i++) { + usbdev_iface_t *fi = &u->ifaces[i]; + if (!fi->claimed || (intf != NULL && fi->intf != intf)) + continue; + for (int p = 1; p <= fi->npipes; p++) + (void) (*fi->intf)->AbortPipe(fi->intf, (UInt8) p); + } + } + return any_inflight; +} + +/* Reopen every endpoint FIFO the shutter closed. All 256 keys and not only the + * ones a pass touched: the endpoints needing a restart are precisely the ones + * the kill did not match. async_lock held. + */ +static void usbdev_restart_eps_locked(usbdev_t *u) +{ + u->draining = false; + for (unsigned k = 0; k < 256; k++) + usbdev_kick_ep_locked(u, (uint8_t) k); +} + +/* Give up on every INFLIGHT URB matching intf whose abort was not answered + * inside the drain deadline: unlink it, settle the slot's accounting and mark + * it orphaned, so what is left of the pending list is what the wire may still + * answer for. async_lock held. + * + * The one implementation of "the deadline passed", because both flavors of reap + * have to leave the slot in the same state on the far side of it: a blocking + * caller reaches here by waiting the deadline out, a non-blocking one by + * measuring it. What that state is, is the reason each step is here. + * + * The slot's own counters are settled now rather than in the late callback, + * which sees orphaned and frees only the record, so a reused slot's counters + * are never touched by it. The record leaves pending, so a later kill or + * teardown scan cannot re-find it and wait another deadline. The owning handle + * is pinned by an orphan count -- the interface's, or the device's for an ep0 + * record that names no interface -- because a whole-device kill unlinks the + * record where the per-interface release rescan would have found it (B1). The + * process-wide budget is not settled here: usbdev_memory_usage accounts live + * bytes, and r->buf is still allocated and still owned by an in-flight IOKit + * transfer, so it is refunded where that buffer is released, in + * urb_free_orphan_locked. + * + * The URB is handed back, which is the whole of CAP_REAP_AFTER_DISCONNECT and + * the one path where it did not hold: the record was unlinked and marked and + * never appended to the completed list, so the same pass that gave up on it + * answered -ENODEV with the URB pointer still unreturned. Linux's + * destroy_all_async completes every pending URB into async_completed before any + * reaper can run, and usb_kill_urb leaves -ENOENT in each, which is what this + * writes. The record stays alive behind the handback until IOKit's callback + * arrives, so the guest gets its pointer back without the late callback ever + * meeting a freed record. + * + * Every FIFO is reopened, and not only the ones this pass touched: an endpoint + * whose leader completed during the drain took the orphaned early return in + * usbdev_async_cb, which starts nothing, so it is left with a queued URB, none + * in flight and no later event to restart it -- its REAPURB never returns. + */ +static void urb_orphan_locked(usbdev_t *u, usbdev_urb_t *r) +{ + urb_pending_unlink(u, r); + u->inflight--; + u->nurbs--; + u->inflight_bytes -= r->charge; + r->orphaned = true; + if (r->intf) { + for (int fi_i = 0; fi_i < USBDEV_MAX_IFACES; fi_i++) { + if (u->ifaces[fi_i].claimed && u->ifaces[fi_i].intf == r->intf) { + u->ifaces[fi_i].orphans++; + break; + } + } + } else { + r->dev_pin = u->dev_serial; + u->dev_orphans++; + } + r->handback = true; + urb_complete_locked(u, r, -LINUX_ENOENT); +} + +static void usbdev_orphan_stalled_locked(usbdev_t *u, + IOUSBInterfaceInterface800 **intf) +{ + int n = 0; + usbdev_urb_t *r = u->pending_head; + while (r) { + usbdev_urb_t *nx = r->next; + if ((intf == NULL || r->intf == intf) && r->state == URB_INFLIGHT) { + urb_orphan_locked(u, r); + n++; + } + r = nx; + } + if (n) + log_warn("usbdev: %d in-flight URB(s) did not drain; orphaned", n); + usbdev_restart_eps_locked(u); +} + +/* Abort and drain every URB whose interface matches intf (NULL = all, including + * ep0). QUEUED records complete as killed and stay reapable; INFLIGHT ones are + * aborted and drained through the callback. + * + * Returns false when the drain timed out: survivors were orphaned (the late + * callback frees them) and the caller must NOT release the IOKit handles they + * still reference. Entry lock held. + */ +static bool usbdev_kill_urbs_locked(usbdev_t *u, + IOUSBInterfaceInterface800 **intf) +{ + (void) usbdev_abort_urbs_locked(u, intf); + + /* Invariant: this wait and the ceiling usbdev_do_reap measures are the same + * USBDEV_DRAIN_TIMEOUT_MS, so a change to it moves both. They are not the + * same clock -- pthread_cond_timedwait wants CLOCK_REALTIME and the ceiling + * is CLOCK_MONOTONIC -- which is why neither is written as an offset from + * the other. + */ + struct timespec deadline; + timespec_deadline_in_ms(&deadline, USBDEV_DRAIN_TIMEOUT_MS); + pthread_mutex_lock(&u->async_lock); + usbdev_urb_t *r; + for (;;) { + bool busy = false; + for (r = u->pending_head; r; r = r->next) { + if ((intf == NULL || r->intf == intf) && r->state == URB_INFLIGHT) { + busy = true; + break; + } + } + if (!busy) + break; + if (pthread_cond_timedwait(&u->async_cv, &u->async_lock, &deadline) == + ETIMEDOUT) { + usbdev_orphan_stalled_locked(u, intf); + pthread_mutex_unlock(&u->async_lock); + return false; + } + } + + /* A per-interface kill leaves other interfaces' queues intact; restart them + * now that the aborts are done. + */ + usbdev_restart_eps_locked(u); + pthread_mutex_unlock(&u->async_lock); + return true; +} + +/* Issue a whole-slot kill's aborts and come straight back. + * + * Linux's proc_reapurbnonblock pops async_getcompleted and answers -EAGAIN or + * -ENODEV: no kill and no wait, because usbdev_remove has already run + * destroy_all_async by the time any reap can see the disconnect. IOKit delivers + * nothing of its own when a device terminates, so this engine owes that kill at + * the first reap that finds the completion list empty -- but running the whole + * of it inline made a non-blocking ioctl sit out the 2 s drain deadline holding + * async_lock against every SUBMITURB and DISCARDURB on the fd (measured against + * a wedged endpoint: REAPURBNDELAY returned -ENODEV after 2006 ms). The aborts + * are what recovers the URBs and they return promptly; the wait is only how the + * caller learns the callbacks landed, and this caller is not allowed to want + * that. Whatever the aborts retire is reapable on a later pass. + * + * Reopening the FIFOs immediately is safe here where it would not be mid-kill: + * every QUEUED record is already retired above, and SUBMITURB on a disconnected + * fd is -ENODEV, so the shutter has nothing left to hold back. Entry lock held. + */ +static void usbdev_abort_urbs_nowait_locked(usbdev_t *u) +{ + (void) usbdev_abort_urbs_locked(u, NULL); + pthread_mutex_lock(&u->async_lock); + usbdev_restart_eps_locked(u); + pthread_mutex_unlock(&u->async_lock); +} + +/* Kill the URBs a device-state change is about to invalidate the handles of, + * and answer whether the change may go ahead: 0 to proceed, -EBUSY to refuse. + * + * The decision belongs here rather than at each call site because the two sites + * that retire handles have to agree, and did not: SETINTERFACE renumbers the + * interface's pipeRefs and SETCONFIGURATION tears the whole pipe table down, so + * an undrained transfer is outstanding at IOKit against a handle the change is + * about to retire either way, yet SETCONFIGURATION discarded the answer and + * changed the device anyway. A drain that misses its 2 s deadline is already an + * abnormal path, so it is refused rather than papered over. Entry lock held. + */ +static int64_t usbdev_drain_for_change(usbdev_t *u, + IOUSBInterfaceInterface800 **intf, + const char *what) +{ + if (usbdev_kill_urbs_locked(u, intf)) + return 0; + log_warn("usbdev: %s refused: URBs did not drain", what); + return -LINUX_EBUSY; +} + +/* Free every reapable completion (fd close: Linux frees unreaped completed URBs + * too). Entry lock held. + */ +static void usbdev_free_completed(usbdev_t *u) +{ + pthread_mutex_lock(&u->async_lock); + while (u->completed_head) { + usbdev_urb_t *rec = u->completed_head; + u->completed_head = rec->next; + if (!u->completed_head) + u->completed_tail = NULL; + if (rec->handback) + urb_handback_taken_locked(rec); + else + urb_free_locked(u, rec); + } + usbdev_ready_settle_locked(u, -1); + pthread_mutex_unlock(&u->async_lock); +} + +/* claim / release (entry lock held) */ + +static int64_t usbdev_claim_locked(usbdev_t *u, unsigned ifnum) +{ + if (ifnum >= USBDEV_MAX_IFACES) + return -LINUX_EINVAL; /* claimintf devio.c:788-789 */ + usbdev_iface_t *fi = &u->ifaces[ifnum]; + if (fi->claimed) + return 0; /* already ours */ + + /* Invariant: a claim this call creates is created against a device the call + * has just reached. The two answers above create none -- the number bound + * and the already-ours short-circuit -- and neither asks; what stands + * behind each of them is the row of that name in + * tests/usbdev-ioctl-departed.tbl. + * + * Ahead of the branch below rather than inside either arm, because only one + * arm asks the device on its own: the registry arm's usbdev_iface_service + * runs the enumeration, and the fixture arm collapses the whole lookup into + * one seam call. usbdev_ensure_dev_plugin stood here and asked nothing -- + * it hands back a cached handle -- so once that arm existed a device that + * had gone granted the claim, and every op that claims implicitly through + * usbdev_pipe_for_ep answered from it: measured on a departed loopback + * device, CLAIMINTERFACE, CLEAR_HALT, RESETEP and SETINTERFACE all returned + * 0 with the fd left unstamped, where Linux's connected() gate makes all + * four -ENODEV. + */ + int64_t drc = usbdev_ensure_dev_reachable(u); + if (drc < 0) + return drc; + + IOUSBInterfaceInterface800 **intf = NULL; + if (u->fake) { + /* One branch for the service lookup, the plugin and USBInterfaceOpen at + * once: all three are IOKit calls with no separately interesting + * answer, and what the lane is about starts after the claim. + */ + int64_t frc = usbdev_fixture_open_iface(u->location_id, ifnum, &intf); + if (frc < 0) + return frc; + goto claimed; + } + + int64_t serr; + io_service_t ifs = usbdev_iface_service(u, ifnum, &serr); + if (ifs == IO_OBJECT_NULL) + return serr < 0 ? serr : -LINUX_ENOENT; + + /* Linux: a bound kernel driver makes CLAIMINTERFACE -EBUSY + * (usb_driver_claim_interface, driver.c:558). macOS arbitration is + * per-IOKit-object and dynamic rather than per-device and static, so the + * bound driver is not the question: USBInterfaceOpen answers + * kIOReturnExclusiveAccess exactly while that driver holds that interface, + * and succeeds while it is idle. Attempt the open and map what IOKit + * answers. Pre-refusing on the mere presence of a driver child refused work + * macOS grants -- the ESP32-S3's CDC data interface opens whenever nothing + * holds /dev/cu.usbmodem1101 -- and it did so from a registry snapshot that + * no live host state corresponds to. + */ + IOCFPlugInInterface **plug = NULL; + SInt32 score = 0; + IOReturn r = IOCreatePlugInInterfaceForService( + ifs, kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID, &plug, + &score); + IOObjectRelease(ifs); + if (r != kIOReturnSuccess || !plug) + return r == kIOReturnSuccess ? -LINUX_ENOMEM : usbdev_ioret_op(u, r); + HRESULT hr = (*plug)->QueryInterface( + plug, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID800), + (LPVOID *) &intf); + (*plug)->Release(plug); + if (hr != S_OK || !intf) + return -LINUX_ENOMEM; + + r = (*intf)->USBInterfaceOpen(intf); + if (r != kIOReturnSuccess) { + (*intf)->Release(intf); + int64_t e = usbdev_ioret_op(u, r); + return e == 0 ? -LINUX_EBUSY : e; + } + +claimed: + /* Publish under async_lock: the late-callback orphan scan reads intf and + * orphans with only that lock held. A stale orphan count belongs to a + * previous (leaked) handle of this slot, so it starts over at zero. + */ + pthread_mutex_lock(&u->async_lock); + fi->intf = intf; + fi->orphans = 0; + pthread_mutex_unlock(&u->async_lock); + int64_t maprc = usbdev_build_pipe_map(u, fi); + if (maprc < 0) { + (*intf)->USBInterfaceClose(intf); + (*intf)->Release(intf); + pthread_mutex_lock(&u->async_lock); + fi->intf = NULL; + pthread_mutex_unlock(&u->async_lock); + return maprc; + } + fi->claimed = true; + claimed_mask_set(u, ifnum); + return 0; +} + +static int64_t usbdev_release_locked(usbdev_t *u, unsigned ifnum) +{ + if (ifnum >= USBDEV_MAX_IFACES) + return -LINUX_EINVAL; + usbdev_iface_t *fi = &u->ifaces[ifnum]; + if (!fi->claimed) { + /* releaseintf checks usb_ifnum_to_if first: a nonexistent interface is + * -ENOENT, an existing unclaimed one -EINVAL (devio.c:815-833). + */ + int64_t drc = usbdev_ensure_dev_plugin(u); + if (drc < 0) + return drc; + int64_t serr; + io_service_t ifs = usbdev_iface_service(u, ifnum, &serr); + if (ifs == IO_OBJECT_NULL) + return serr < 0 ? serr : -LINUX_ENOENT; + IOObjectRelease(ifs); + return -LINUX_EINVAL; + } + + /* proc_releaseinterface kills the interface's URBs, through + * destroy_async_on_interface once releaseintf has succeeded + * (devio.c:2305-2317); releaseintf itself only drops the driver claim and + * kills nothing. The kill is a usb_kill_urb each, so they stay reapable + * with -ENOENT. + */ + bool drained = usbdev_kill_urbs_locked(u, fi->intf); + + /* A whole-device drain timeout (fd teardown) orphaned this interface's + * survivors after unlinking them from pending, so the rescan above came + * back clean in under a wait; only the orphan count still knows the handle + * is referenced (B1). + */ + pthread_mutex_lock(&u->async_lock); + if (fi->orphans != 0) + drained = false; + pthread_mutex_unlock(&u->async_lock); + if (fi->src) { + CFRunLoopRef loop = usbdev_loop_current(); + if (loop && drained) + CFRunLoopRemoveSource(loop, fi->src, kCFRunLoopDefaultMode); + if (drained) + CFRelease(fi->src); + fi->src = NULL; + } + if (drained) { + (*fi->intf)->USBInterfaceClose(fi->intf); + (*fi->intf)->Release(fi->intf); + } else { + /* Orphaned in-flight URBs still reference the handle and its event + * source; leak both rather than hand the late callback a freed plugin. + */ + log_warn("usbdev: leaking interface %u handle (undrained URBs)", ifnum); + } + pthread_mutex_lock(&u->async_lock); + fi->intf = NULL; + pthread_mutex_unlock(&u->async_lock); + fi->claimed = false; + claimed_mask_clear(u, ifnum); + fi->npipes = 0; + return 0; +} + +/* CLAIMINTERFACE and RELEASEINTERFACE as ops of their own, which is not the + * same question as the implicit claim usbdev_pipe_for_ep and check_ctrlrecip + * take on a transfer's behalf. + * + * An interface this fd already holds is answered above out of this layer's own + * bookkeeping, which stays true after the device leaves. For an implicit claim + * that is right and costs nothing: the transfer behind it is what asks, and it + * asks a moment later. For these two the claim is the whole op, so there is no + * transfer behind it and the answer was the stale one -- measured with an fd + * that took its claim before the device left and armed no disconnect watch, + * CLAIMINTERFACE and RELEASEINTERFACE both returned 0 with the fd unstamped and + * poll silent, where proc_claiminterface and proc_releaseinterface sit behind + * usbdev_do_ioctl's connected() gate and Linux answers -ENODEV. The rows are + * CLAIMINTERFACE_HELD and RELEASEINTERFACE_HELD. + * + * Only that arm asks. An unheld number reaches usbdev_ensure_dev_reachable + * inside the claim, or usbdev_iface_service inside the release, so nothing here + * enumerates twice; and the interface-number bound stays ahead of the question, + * which is the deviation docs/internals.md records and CLAIMINTERFACE_BOUND + * pins. + * + * Not on the teardown path: usbdev_teardown_locked and the USBDEVFS_IOCTL + * unbind call usbdev_release_locked directly, so a close still drops the claim + * and closes the handle on a device that has gone. Entry lock held. + */ +static int64_t usbdev_claim_ioctl(usbdev_t *u, unsigned ifnum) +{ + if (ifnum < USBDEV_MAX_IFACES && u->ifaces[ifnum].claimed) + return usbdev_ensure_dev_reachable(u); + return usbdev_claim_locked(u, ifnum); +} + +static int64_t usbdev_release_ioctl(usbdev_t *u, unsigned ifnum) +{ + if (ifnum < USBDEV_MAX_IFACES && u->ifaces[ifnum].claimed) { + int64_t drc = usbdev_ensure_dev_reachable(u); + if (drc < 0) + return drc; + } + return usbdev_release_locked(u, ifnum); +} + +/* usbdev_ep_owner_iface's two failures, kept apart because Linux answers them + * differently: an endpoint no altsetting carries is -ENOENT (findintfep's own + * return), while one whose owning interface number is past the claim bitmap is + * -EINVAL (checkintf, devio.c:845-846). + */ +#define USBDEV_EP_OWNER_NONE (-1) +#define USBDEV_EP_OWNER_OUT_OF_RANGE (-2) + +/* findintfep (devio.c:856-879): which interface of the active config carries + * bEndpointAddress ep, searching every altsetting. Parsed from the descriptors + * blob. + * + * Returns USBDEV_EP_OWNER_NONE when not found, or USBDEV_EP_OWNER_OUT_OF_RANGE + * when the endpoint is carried by an interface number this layer cannot + * represent. Never returns a number ifaces[] does not hold. + */ +static int usbdev_ep_owner_iface(const usbdev_t *u, uint8_t ep) +{ + const uint8_t *b = u->blob; + size_t len = u->blob_len; + size_t off = 18; + while (off + 9 <= len && b[off + 1] == 0x02 /* CONFIG */) { + size_t total = (size_t) b[off + 2] | ((size_t) b[off + 3] << 8); + if (total < 9 || off + total > len) + break; + bool active = b[off + 5] == (uint8_t) u->cfg_value; if (active) { size_t p = off + 9; int cur_if = USBDEV_EP_OWNER_NONE; @@ -967,19 +2694,22 @@ static void usbdev_unref(usbdev_t *u) * unrelated fds, opens of other devices, and close(), which needs the same * table lock. Measured: an 18.6 s close() of an unrelated fd behind one 20 s * transfer. The in-code claim that this "briefly" stalled other fds' lookups - * was neither brief nor bounded. + * was neither brief nor bounded. The side-table half, for a caller that already + * holds the fd-table snapshot the entry has to answer for -- one that pinned a + * descriptor from it, say. Taking a second snapshot inside here instead would + * prove the generation against a window of its own, so a close and reopen in + * between would hand back the entry of the new description while the caller + * went on using the old one's descriptor. The whole point of the parameter is + * that this proves the caller's window and not another. */ -static usbdev_t *usbdev_acquire(int fd) +static usbdev_t *usbdev_acquire_snap(int fd, const fd_entry_t *snap) { - fd_entry_t snap; - if (!fd_snapshot(fd, &snap) || snap.type != FD_USBDEV) - return NULL; usbdev_t *u = NULL; pthread_mutex_lock(&usbdev_table_lock); for (int i = 0; i < USBDEV_MAX_FDS; i++) { if (usbdev_fds[i].used && !usbdev_fds[i].dead && usbdev_fds[i].guest_fd == fd && - usbdev_fds[i].generation == snap.generation) { + usbdev_fds[i].generation == snap->generation) { u = &usbdev_fds[i]; u->refs++; break; @@ -997,22 +2727,73 @@ static usbdev_t *usbdev_acquire(int fd) return u; } -/* Unlock and unpin an entry usbdev_acquire returned. */ +static usbdev_t *usbdev_acquire(int fd) +{ + fd_entry_t snap; + if (!fd_snapshot(fd, &snap) || snap.type != FD_USBDEV) + return NULL; + return usbdev_acquire_snap(fd, &snap); +} + +/* Unlock and unpin an entry usbdev_acquire returned. + * + * The cross-fd disconnect walk is paid off in between, which is the whole + * reason this is not two calls at each site: it needs the entry lock gone (it + * takes the table lock, which never nests under an entry lock) and the pin + * still held (it reads this slot's devkey). Every ioctl that can stamp a + * disconnect leaves through here. + */ static void usbdev_release(usbdev_t *u) { pthread_mutex_unlock(&u->lock); + usbdev_flush_disc_peers(u); usbdev_unref(u); } static void usbdev_teardown_locked(usbdev_t *u) { + /* Async teardown first: kill/drain every URB (release close: Linux kills + * pending and frees completed, devio.c:1092-1128), then the notification + * and the ep0 event source, so no callback can arrive for this slot after + * the handles go away. + */ + bool drained = usbdev_kill_urbs_locked(u, NULL); + + /* The device handle's half of the same question usbdev_release_locked asks + * of an interface's: a whole-slot drain timeout unlinks its survivors, so + * the rescan inside the kill comes back clean and only the orphan count + * still knows u->dev and u->dev_src are referenced. + */ + pthread_mutex_lock(&u->async_lock); + if (u->dev_orphans != 0) + drained = false; + pthread_mutex_unlock(&u->async_lock); + usbdev_free_completed(u); + if (u->fake) + usbdev_fixture_unwatch(usbdev_watch_token(u)); + if (u->notif != IO_OBJECT_NULL) { + IOObjectRelease(u->notif); + u->notif = IO_OBJECT_NULL; + } + if (u->dev_src) { + CFRunLoopRef loop = usbdev_loop_current(); + if (loop && drained) + CFRunLoopRemoveSource(loop, u->dev_src, kCFRunLoopDefaultMode); + if (drained) + CFRelease(u->dev_src); + u->dev_src = NULL; + } for (int i = 0; i < USBDEV_MAX_IFACES; i++) if (u->ifaces[i].claimed) (void) usbdev_release_locked(u, (unsigned) i); if (u->dev) { - if (u->dev_open) - (*u->dev)->USBDeviceClose(u->dev); - (*u->dev)->Release(u->dev); + if (drained) { + if (u->dev_open) + (*u->dev)->USBDeviceClose(u->dev); + (*u->dev)->Release(u->dev); + } else { + log_warn("usbdev: leaking device handle (undrained URBs)"); + } u->dev = NULL; } u->dev_open = false; @@ -1021,10 +2802,30 @@ static void usbdev_teardown_locked(usbdev_t *u) IOObjectRelease(u->service); u->service = IO_OBJECT_NULL; } - if (u->pipe_wr >= 0) { - close(u->pipe_wr); - u->pipe_wr = -1; - } + u->fake = false; + + /* Orphaned callbacks skip the pipe write, so closing it here is safe even + * on the timeout path; -1 under async_lock keeps the callback's check and + * this close ordered. + */ + pthread_mutex_lock(&u->async_lock); + int pw = u->pipe_wr; + u->pipe_wr = -1; + + /* The pipe goes with the fd, so the token goes with it; the four disconnect + * fields reset together here for the same reason they do in the open path. + * Clearing disc_peers_pending is a slot reset, not a payment -- a caller + * with a debt to settle takes it with usbdev_take_disc_debt before calling + * this, because the walk it owes cannot run under the entry lock. + */ + u->ready_token = false; + u->disconnected = false; + u->disc_drained = false; + u->disc_orphan_at_ms = 0; + atomic_store_explicit(&u->disc_peers_pending, false, memory_order_relaxed); + pthread_mutex_unlock(&u->async_lock); + if (pw >= 0) + close(pw); free(u->blob); u->blob = NULL; @@ -1072,10 +2873,40 @@ static void usbdev_fd_cleanup(int guest_fd) /* Outside the table lock: a sync transfer in flight on this fd holds the * entry lock, and waiting for it here must not block every other fd. + * + * The debt comes off the slot before the teardown that would clear it and + * retire the devkey the walk needs, and is paid once the entry lock is + * down: a stamp this fd took and had not yet published -- an async + * callback's, between its own unlock and its own flush -- still has peers + * to tell, and the closing fd is the one that will never ask again. */ pthread_mutex_lock(&u->lock); + uint64_t debt = usbdev_take_disc_debt(u); usbdev_teardown_locked(u); pthread_mutex_unlock(&u->lock); + usbdev_pay_disc_debt(debt, u); + + /* Clear the poll maps before the slot is released, and only while no live + * entry answers to this fd number. fd_cleanup_entry runs after the number + * is free for reuse, so a sibling's open() can already have bound a new + * usbdevfs fd here; clearing unconditionally erased that fd's disconnect + * bit and left its poll/select/epoll silent for good, and clearing after + * usbdev_unref left the same window open against a freshly reused slot. + */ + if (RANGE_CHECK(guest_fd, 0, FD_TABLE_SIZE)) { + pthread_mutex_lock(&usbdev_table_lock); + bool live = false; + for (int i = 0; i < USBDEV_MAX_FDS; i++) { + usbdev_t *o = &usbdev_fds[i]; + if (o != u && o->used && !o->dead && o->guest_fd == guest_fd) + live = true; + } + if (!live) { + discmap_clear(guest_fd); + readymap_clear(guest_fd); + } + pthread_mutex_unlock(&usbdev_table_lock); + } usbdev_unref(u); } @@ -1087,7 +2918,10 @@ void usbdev_init(void) memset(&usbdev_fds[i], 0, sizeof(usbdev_fds[i])); usbdev_fds[i].guest_fd = -1; usbdev_fds[i].pipe_wr = -1; + usbdev_fds[i].notif = IO_OBJECT_NULL; pthread_mutex_init(&usbdev_fds[i].lock, NULL); + pthread_mutex_init(&usbdev_fds[i].async_lock, NULL); + pthread_cond_init(&usbdev_fds[i].async_cv, NULL); } usbdev_ready = true; } @@ -1205,8 +3039,10 @@ static void usbdev_retire_unpublished(usbdev_t *u, int guest_fd, uint64_t gen) if (!mine) return; pthread_mutex_lock(&u->lock); + uint64_t debt = usbdev_take_disc_debt(u); usbdev_teardown_locked(u); pthread_mutex_unlock(&u->lock); + usbdev_pay_disc_debt(debt, u); usbdev_unref(u); } @@ -1357,14 +3193,44 @@ int64_t usbdev_open_path(const char *path, int linux_flags) u->pos = 0; u->pipe_wr = pipefd[1]; u->service = IO_OBJECT_NULL; + u->fake = false; u->dev = NULL; + u->dev_orphans = 0; + u->dev_serial = 0; u->dev_open = false; u->dev_open_tried = false; + u->dev_src = NULL; + u->notif = IO_OBJECT_NULL; memset(u->ifaces, 0, sizeof(u->ifaces)); claimed_mask_reset(u); /* Nonzero while bound; equal for every fd open on the same device node. */ devkey_publish( u, (1ull << 63) | ((uint64_t) (uint32_t) bus << 32) | (uint32_t) dev); + u->pending_head = u->pending_tail = NULL; + memset(u->ep_head, 0, sizeof(u->ep_head)); + memset(u->ep_tail, 0, sizeof(u->ep_tail)); + u->completed_head = u->completed_tail = NULL; + u->inflight = 0; + u->nurbs = 0; + u->inflight_bytes = 0; + u->ready_token = false; + + /* All four fields of the disconnect state, together: a slot whose previous + * open drained after a disconnect would otherwise start this open with the + * drain already claimed, and this open's own disconnect would issue no + * aborts; a deadline left behind would have this open's first reap after a + * disconnect orphan URBs the aborts had not been given their deadline to + * answer for; an unpaid peer-walk debt left behind would have this open's + * first ioctl stamp the whole device off the previous open's disconnect. + * Nothing else clears them outside teardown -- usbdev_init zeroes the table + * once, at startup. + */ + u->disconnected = false; + u->disc_drained = false; + u->disc_orphan_at_ms = 0; + atomic_store_explicit(&u->disc_peers_pending, false, memory_order_relaxed); + u->discsig_signr = 0; + u->discsig_context = 0; pthread_mutex_unlock(&u->lock); /* fd_alloc_from's out_gen, not a later read of the slot: the generation has @@ -1387,6 +3253,15 @@ int64_t usbdev_open_path(const char *path, int linux_flags) return -LINUX_EMFILE; } + /* Before anything else this fd number can be polled through: the previous + * owner's cleanup runs after the number is free for reuse, so a bit it left + * behind would make a brand-new healthy fd report POLLERR|POLLHUP. + */ + if (RANGE_CHECK(guest_fd, 0, FD_TABLE_SIZE)) { + discmap_clear(guest_fd); + readymap_clear(guest_fd); + } + /* Stamp the node path so /proc/self/fd/N readlink reports the guest * spelling (stage-1 mechanism), and publish the fd's flags. */ @@ -1472,6 +3347,11 @@ int64_t usbdev_read(int fd, guest_t *g, uint64_t buf_gva, uint64_t count) return -LINUX_EBADF; if (!(usbdev_fmode(snap.linux_flags) & USBDEV_FMODE_READ)) return -LINUX_EBADF; /* vfs: read needs FMODE_READ */ + /* usbdev_read serves nothing once the device is gone (-ENODEV, + * devio.c:323-325); lseek keeps working, as its llseek has no gate. + */ + if (usbdev_fd_disconnected(fd)) + return -LINUX_ENODEV; usbdev_t *u = usbdev_acquire(fd); if (!u) return -LINUX_EBADF; @@ -1510,6 +3390,9 @@ int64_t usbdev_pread(int fd, return -LINUX_EBADF; if (!(usbdev_fmode(snap.linux_flags) & USBDEV_FMODE_READ)) return -LINUX_EBADF; /* vfs: read needs FMODE_READ */ + /* Same -ENODEV gate as usbdev_read: no read path serves a gone device. */ + if (usbdev_fd_disconnected(fd)) + return -LINUX_ENODEV; usbdev_t *u = usbdev_acquire(fd); if (!u) return -LINUX_EBADF; @@ -1639,57 +3522,69 @@ static int64_t usbdev_do_control(usbdev_t *u, guest_t *g, uint64_t arg) if (ct.wLength > USBDEV_CTRL_MAX) return -LINUX_EINVAL; - int64_t rc = usbdev_ensure_dev_plugin(u); - if (rc < 0) - return rc; - usbdev_lazy_device_open(u); + /* The allowance, taken where do_proc_control takes it: after the recipient + * check and the wLength cap, before the buffer this transfer bounces + * through, and given back on every exit from here down. Charging nothing + * here was the one synchronous path outside the allowance: measured with + * 16771328 bytes in flight, a sync BULK was refused -ENOMEM and a sync + * CONTROL of wLength 4096 went through and reported 4096. Every exit below + * the charge is a single one for that reason, the way the bulk path's is. + */ + if (!usbdev_memory_charge(USBDEV_CTRL_CHARGE)) + return -LINUX_ENOMEM; uint8_t *buf = NULL; - if (ct.wLength > 0) { - buf = malloc(ct.wLength); - if (!buf) - return -LINUX_ENOMEM; - } - bool in = (ct.bRequestType & 0x80) != 0; - if (!in && ct.wLength > 0 && guest_read(g, ct.data, buf, ct.wLength) < 0) { - free(buf); - return -LINUX_EFAULT; - } - - IOUSBDevRequestTO req = { - .bmRequestType = ct.bRequestType, - .bRequest = ct.bRequest, - .wValue = ct.wValue, - .wIndex = ct.wIndex, - .wLength = ct.wLength, - .pData = buf, - .noDataTimeout = ct.timeout, - .completionTimeout = ct.timeout, - }; - IOReturn r = (*u->dev)->DeviceRequestTO(u->dev, &req); - if ((uint32_t) r == (uint32_t) kIOReturnNotOpen && !u->dev_open) { - /* Some requests demand an open device; retry once after opening. */ - IOReturn ro = (*u->dev)->USBDeviceOpen(u->dev); - if (ro == kIOReturnSuccess) { - u->dev_open = true; - r = (*u->dev)->DeviceRequestTO(u->dev, &req); - } - } - int64_t err = ioret_neg_errno(r); - if (err < 0) { - /* On -ETIMEDOUT/-EINTR partial IN data is NOT copied out - * (devio.c:1227). - */ - free(buf); - return err; + bool in = (ct.bRequestType & 0x80) != 0; + int64_t ret = usbdev_ensure_dev_plugin(u); + if (ret >= 0) { + usbdev_lazy_device_open(u); + ret = 0; + if (ct.wLength > 0) { + buf = malloc(ct.wLength); + if (!buf) + ret = -LINUX_ENOMEM; + } } - int64_t actlen = req.wLenDone; - if (in && actlen > 0 && guest_write(g, ct.data, buf, (size_t) actlen) < 0) { - free(buf); - return -LINUX_EFAULT; + if (ret >= 0 && !in && ct.wLength > 0 && + guest_read(g, ct.data, buf, ct.wLength) < 0) + ret = -LINUX_EFAULT; + if (ret >= 0) { + IOUSBDevRequestTO req = { + .bmRequestType = ct.bRequestType, + .bRequest = ct.bRequest, + .wValue = ct.wValue, + .wIndex = ct.wIndex, + .wLength = ct.wLength, + .pData = buf, + .noDataTimeout = ct.timeout, + .completionTimeout = ct.timeout, + }; + IOReturn r = (*u->dev)->DeviceRequestTO(u->dev, &req); + if ((uint32_t) r == (uint32_t) kIOReturnNotOpen && !u->dev_open) { + /* Some requests demand an open device; retry once after opening. */ + IOReturn ro = (*u->dev)->USBDeviceOpen(u->dev); + if (ro == kIOReturnSuccess) { + u->dev_open = true; + r = (*u->dev)->DeviceRequestTO(u->dev, &req); + } + } + int64_t err = usbdev_ioret_op(u, r); + if (err < 0) { + /* On -ETIMEDOUT/-EINTR partial IN data is NOT copied out + * (devio.c:1230). + */ + ret = err; + } else { + int64_t actlen = req.wLenDone; + ret = in && actlen > 0 && + guest_write(g, ct.data, buf, (size_t) actlen) < 0 + ? -LINUX_EFAULT + : actlen; + } } free(buf); - return actlen; + usbdev_memory_refund(USBDEV_CTRL_CHARGE); + return ret; } static int64_t usbdev_do_bulk(usbdev_t *u, guest_t *g, uint64_t arg) @@ -1752,7 +3647,7 @@ static int64_t usbdev_do_bulk(usbdev_t *u, guest_t *g, uint64_t arg) UInt32 size = bt.len; IOReturn r = (*fi->intf)->ReadPipeTO(fi->intf, pipe, buf, &size, bt.timeout, bt.timeout); - int64_t err = ioret_neg_errno(r); + int64_t err = usbdev_ioret_op(u, r); if (err < 0) { ret = err; /* partial data not copied on error, as Linux */ } else if (size > 0 && guest_write(g, bt.data, buf, size) < 0) { @@ -1775,7 +3670,7 @@ static int64_t usbdev_do_bulk(usbdev_t *u, guest_t *g, uint64_t arg) bt.ep); ret = -LINUX_EIO; } else { - int64_t err = ioret_neg_errno(r); + int64_t err = usbdev_ioret_op(u, r); ret = err < 0 ? err : bt.len; } } @@ -1813,7 +3708,7 @@ static int64_t usbdev_do_getdriver(usbdev_t *u, guest_t *g, uint64_t arg) /* Ahead of everything below: an interface question about a device that is * not reachable is -ENODEV, not "no driver" (devio.c's connected() gate). */ - int64_t drc = usbdev_ensure_dev_plugin(u); + int64_t drc = usbdev_ensure_dev_reachable(u); if (drc < 0) return drc; @@ -1831,9 +3726,10 @@ static int64_t usbdev_do_getdriver(usbdev_t *u, guest_t *g, uint64_t arg) usbdev_iface_claimed_elsewhere(u, gd.interface)) { str_copy_trunc(gd.driver, "usbfs", sizeof(gd.driver)); } else { - io_service_t ifs = usbdev_iface_service(u, gd.interface); + int64_t serr; + io_service_t ifs = usbdev_iface_service(u, gd.interface, &serr); if (ifs == IO_OBJECT_NULL) - return -LINUX_ENODATA; /* usb_ifnum_to_if NULL */ + return serr < 0 ? serr : -LINUX_ENODATA; /* usb_ifnum_to_if NULL */ bool bound = usbdev_iface_driver(ifs, gd.driver, sizeof(gd.driver)); IOObjectRelease(ifs); if (!bound) @@ -1865,10 +3761,17 @@ static int64_t usbdev_do_setinterface(usbdev_t *u, guest_t *g, uint64_t arg) if (si.altsetting > 0xff) return -LINUX_EINVAL; usbdev_iface_t *fi = &u->ifaces[si.interface]; + + /* proc_setintf kills the interface's URBs before switching altsettings + * (devio.c:1529-1544); in-flight pipeRefs die with the old pipe table. + */ + rc = usbdev_drain_for_change(u, fi->intf, "SETINTERFACE"); + if (rc < 0) + return rc; IOReturn r = (*fi->intf)->SetAlternateInterface(fi->intf, (UInt8) si.altsetting); if (r != kIOReturnSuccess) { - int64_t err = ioret_neg_errno(r); + int64_t err = usbdev_ioret_op(u, r); log_debug("usbdev: SetAlternateInterface(%u, %u) -> 0x%x", si.interface, si.altsetting, r); @@ -1884,7 +3787,7 @@ static int64_t usbdev_do_setinterface(usbdev_t *u, guest_t *g, uint64_t arg) err = -LINUX_EINVAL; return err; } - return usbdev_build_pipe_map(fi); + return usbdev_build_pipe_map(u, fi); } /* proc_setconfig's claim check is device-wide (usb_interface_claimed, @@ -1926,41 +3829,49 @@ static int64_t usbdev_do_setconfiguration(usbdev_t *u, guest_t *g, uint64_t arg) return -LINUX_EBUSY; if (usbdev_claimed_elsewhere(u)) return -LINUX_EBUSY; - int64_t rc = usbdev_ensure_dev_plugin(u); - if (rc < 0) - return rc; /* A bound host (Apple) driver claims its interface exactly like a Linux * driver would (usb_interface_claimed covers every driver, not just usbfs): * one iterator pass over the device's interfaces. + * + * Invariant: an enumeration that did not run is that failure's answer, not + * "no driver is bound". Discarding it here let a departed device reach + * SetConfiguration with bound == false, and SETCONFIGURATION then answered + * whatever the wire made of a config change on a device that was gone, + * stamping nothing -- where Linux's connected() gate answers -ENODEV. It is + * also the only check standing between a bound host driver and a + * configuration change. */ - IOUSBFindInterfaceRequest fr = { - .bInterfaceClass = kIOUSBFindInterfaceDontCare, - .bInterfaceSubClass = kIOUSBFindInterfaceDontCare, - .bInterfaceProtocol = kIOUSBFindInterfaceDontCare, - .bAlternateSetting = kIOUSBFindInterfaceDontCare, - }; - io_iterator_t it = IO_OBJECT_NULL; - if ((*u->dev)->CreateInterfaceIterator(u->dev, &fr, &it) == - kIOReturnSuccess) { - bool bound = false; - io_service_t svc; - while ((svc = IOIteratorNext(it))) { - char drv[64]; - if (!bound && usbdev_iface_driver(svc, drv, sizeof(drv))) - bound = true; - IOObjectRelease(svc); - } - IOObjectRelease(it); - if (bound) - return -LINUX_EBUSY; + int64_t rc; + io_iterator_t it = usbdev_iface_iterator(u, &rc); + if (rc < 0) + return rc; + bool bound = false; + io_service_t svc; + while ((svc = IOIteratorNext(it))) { + char drv[64]; + if (!bound && usbdev_iface_driver(svc, drv, sizeof(drv))) + bound = true; + IOObjectRelease(svc); } + IOObjectRelease(it); + if (bound) + return -LINUX_EBUSY; usbdev_lazy_device_open(u); if (!u->dev_open) return -LINUX_EBUSY; /* exclusive holder elsewhere */ + + /* usb_set_configuration -> usb_disable_device kills every URB on the + * device, ep0 included. The -EBUSY check above only looks at interfaces, + * and the default control pipe is exactly the queue no interface claim + * covers, so a control URB could otherwise ride across the change. + */ + rc = usbdev_drain_for_change(u, NULL, "SETCONFIGURATION"); + if (rc < 0) + return rc; IOReturn r = (*u->dev)->SetConfiguration(u->dev, (UInt8) cfg); if (r != kIOReturnSuccess) { - int64_t err = ioret_neg_errno(r); + int64_t err = usbdev_ioret_op(u, r); return err == -LINUX_ENOENT ? -LINUX_EINVAL : err; } u->cfg_value = cfg; @@ -1979,9 +3890,21 @@ static int64_t usbdev_do_clear_halt(usbdev_t *u, guest_t *g, uint64_t arg) return rc; /* ClearPipeStallBothEnds == CLEAR_FEATURE(ENDPOINT_HALT) + host-side toggle - * reset (IOUSBLib.h:2928-2941), exactly usb_clear_halt. + * reset (IOUSBLib.h:2927-2942), exactly usb_clear_halt on the wire. + * + * It also aborts whatever is outstanding on that pipe, and IOKit exposes no + * variant that does not. Linux's check_reset_of_active_ep (devio.c: + * 1382-1394) only dev_warn()s and leaves the queue alone, so an async URB + * parked on the endpoint survives a CLEAR_HALT there and reaps -ECONNRESET + * here. libusb calls libusb_clear_halt between transfers, so this is + * reachable in ordinary use; it is a printed XFAIL in + * tests/test-usbdev-ioctl.c and a bullet in the deviations list under USB + * Device Passthrough in docs/internals.md -- not a row of the Deviations + * From Linux table further down, which does not carry it -- rather than + * something the engine can fix. */ - return ioret_neg_errno((*fi->intf)->ClearPipeStallBothEnds(fi->intf, pipe)); + return usbdev_ioret_op(u, + (*fi->intf)->ClearPipeStallBothEnds(fi->intf, pipe)); } static int64_t usbdev_do_resetep(usbdev_t *u, guest_t *g, uint64_t arg) @@ -1999,6 +3922,15 @@ static int64_t usbdev_do_disconnect_claim(usbdev_t *u, guest_t *g, uint64_t arg) if (guest_read_small(g, arg, &dc, sizeof(dc)) < 0) return -LINUX_EFAULT; + /* Ahead of the interface-number check, not merely ahead of the interface + * answer: usbdev_do_ioctl's connected() gate runs before + * proc_disconnect_claim is entered at all, so a device that is gone is + * -ENODEV for every interface number, in range or out of it. + */ + int64_t drc = usbdev_ensure_dev_reachable(u); + if (drc < 0) + return drc; + /* proc_disconnect_claim has no range check of its own: usb_ifnum_to_if * answers for the number and a NULL result is -EINVAL (devio.c:2467-2469), * which is the opposite of claimintf's -ENOENT for the same shape. @@ -2006,9 +3938,6 @@ static int64_t usbdev_do_disconnect_claim(usbdev_t *u, guest_t *g, uint64_t arg) if (dc.interface >= USBDEV_MAX_IFACES) return -LINUX_EINVAL; dc.driver[sizeof(dc.driver) - 1] = '\0'; - int64_t drc = usbdev_ensure_dev_plugin(u); - if (drc < 0) - return drc; char drv[256] = ""; bool bound = false; @@ -2017,9 +3946,10 @@ static int64_t usbdev_do_disconnect_claim(usbdev_t *u, guest_t *g, uint64_t arg) str_copy_trunc(drv, "usbfs", sizeof(drv)); bound = true; } else { - io_service_t ifs = usbdev_iface_service(u, dc.interface); + int64_t serr; + io_service_t ifs = usbdev_iface_service(u, dc.interface, &serr); if (ifs == IO_OBJECT_NULL) - return -LINUX_EINVAL; + return serr < 0 ? serr : -LINUX_EINVAL; bound = usbdev_iface_driver(ifs, drv, sizeof(drv)); IOObjectRelease(ifs); } @@ -2046,12 +3976,17 @@ static int64_t usbdev_do_driver_ioctl(usbdev_t *u, guest_t *g, uint64_t arg) linux_usbdevfs_ioctl_t ic; if (guest_read_small(g, arg, &ic, sizeof(ic)) < 0) return -LINUX_EFAULT; + + /* Ahead of the interface-number check, for the reason + * usbdev_do_disconnect_claim gives: connected() runs before proc_ioctl, + * which then repeats it for itself before it looks the number up. + */ + int64_t drc = usbdev_ensure_dev_reachable(u); + if (drc < 0) + return drc; if (ic.ifno < 0 || ic.ifno >= USBDEV_MAX_IFACES) return -LINUX_EINVAL; unsigned ifnum = (unsigned) ic.ifno; - int64_t drc = usbdev_ensure_dev_plugin(u); - if (drc < 0) - return drc; switch ((uint32_t) ic.ioctl_code) { case USBDEVFS_IOCTL_DISCONNECT: { if (u->ifaces[ifnum].claimed) @@ -2063,9 +3998,10 @@ static int64_t usbdev_do_driver_ioctl(usbdev_t *u, guest_t *g, uint64_t arg) */ return -LINUX_EBUSY; } - io_service_t ifs = usbdev_iface_service(u, ifnum); + int64_t serr; + io_service_t ifs = usbdev_iface_service(u, ifnum, &serr); if (ifs == IO_OBJECT_NULL) - return -LINUX_EINVAL; + return serr < 0 ? serr : -LINUX_EINVAL; char drv[64]; bool bound = usbdev_iface_driver(ifs, drv, sizeof(drv)); IOObjectRelease(ifs); @@ -2083,9 +4019,10 @@ static int64_t usbdev_do_driver_ioctl(usbdev_t *u, guest_t *g, uint64_t arg) if (u->ifaces[ifnum].claimed || usbdev_iface_claimed_elsewhere(u, ifnum)) return -LINUX_EBUSY; - io_service_t ifs = usbdev_iface_service(u, ifnum); + int64_t serr; + io_service_t ifs = usbdev_iface_service(u, ifnum, &serr); if (ifs == IO_OBJECT_NULL) - return -LINUX_EINVAL; + return serr < 0 ? serr : -LINUX_EINVAL; char drv[64]; bool bound = usbdev_iface_driver(ifs, drv, sizeof(drv)); IOObjectRelease(ifs); @@ -2098,6 +4035,710 @@ static int64_t usbdev_do_driver_ioctl(usbdev_t *u, guest_t *g, uint64_t arg) } } +/* async URB ioctls */ + +/* SUBMITURB (proc_do_submiturb, doc A section A3). Entry lock held. */ +static int64_t usbdev_do_submiturb(usbdev_t *u, guest_t *g, uint64_t arg) +{ + linux_usbdevfs_urb_t uu; + if (guest_read_small(g, arg, &uu, sizeof(uu)) < 0) + return -LINUX_EFAULT; + + /* The flags mask, USBFS_XFER_MAX and the null buffer, in the kernel's order + * (usbdev-urb.h). A negative buffer_length is covered by the unsigned + * compare, exactly as it is in devio.c:1647. + */ + int64_t argrc = usbdev_urb_arg_check(uu.type, uu.flags, uu.buffer_length, + uu.buffer == 0); + if (argrc < 0) + return argrc; + + bool is_in; + uint32_t data_len; + uint64_t data_gva; + usbdev_iface_t *fi = NULL; + uint8_t pipe = 0; + bool pipe_interrupt = false; + uint16_t mps = 0; + IOUSBDevRequestTO req; + memset(&req, 0, sizeof(req)); + + /* Resolve the endpoint before the per-type checks, and before the control + * arm's own length rule. proc_do_submiturb runs findintfep + checkintf + + * ep_to_host_endpoint at devio.c:1651-1661 and only then switches on the + * type, so an endpoint that does not exist is -ENOENT no matter how + * malformed the rest of the request is. pr-c's synchronous do_proc_bulk + * already carries that rule in a comment of its own; the async path was + * written the other way round and answered -EINVAL for requests Linux + * rejects by endpoint. Default-control-pipe URBs skip the lookup, which is + * the same exemption the kernel writes at devio.c:1651. + */ + bool ep0_control = + uu.type == LINUX_URB_TYPE_CONTROL && (uu.endpoint & 0x7f) == 0; + if (!ep0_control) { + int64_t rc = usbdev_pipe_for_ep(u, uu.endpoint, &fi, &pipe); + if (rc < 0) + return rc; + } + + switch (uu.type) { + case LINUX_URB_TYPE_CONTROL: + case LINUX_URB_TYPE_BULK: + case LINUX_URB_TYPE_INTERRUPT: + break; + case LINUX_URB_TYPE_ISO: + log_warn("usbdev: ISO URBs unsupported (doc A section A7.4)"); + return -LINUX_EINVAL; + default: + return -LINUX_EINVAL; + } + + /* Accepted and never raised: elfuse has no async guest-signal injection + * from the event thread, so the completion signal proc_do_submiturb arms + * (async_completed -> kill_pid_usb_asyncio, devio.c:657) is a documented + * gap, printed as an XFAIL by tests/test-usbdev-ioctl.c beside + * DISCSIGNAL's. + */ + if (uu.signr) + log_warn( + "usbdev: URB completion signal %u accepted but never delivered", + uu.signr); + + if (uu.type == LINUX_URB_TYPE_CONTROL) { + /* Buffer = 8-byte setup + wLength data (devio.c:1671-1683). */ + uint8_t setup[8]; + if (uu.buffer_length < 8) + return -LINUX_EINVAL; + if (guest_read_small(g, uu.buffer, setup, sizeof(setup)) < 0) + return -LINUX_EFAULT; + uint16_t wLength = (uint16_t) (setup[6] | (setup[7] << 8)); + if ((uint32_t) uu.buffer_length - 8 < wLength) + return -LINUX_EINVAL; + + /* check_ctrlrecip: vendor requests bypass; IF/EP recipients claim the + * owning interface implicitly (devio.c:881-938). + */ + if ((setup[0] & 0x60) != 0x40) { + unsigned recip = setup[0] & 0x1f; + uint16_t wIndex = (uint16_t) (setup[4] | (setup[5] << 8)); + if (recip == 1) { + int64_t rc = usbdev_claim_locked(u, wIndex & 0xff); + if (rc < 0) + return rc; + } else if (recip == 2) { + int64_t rc = usbdev_check_ep_recip(u, wIndex); + if (rc < 0) + return rc; + } + } + + /* Zero-length control IN is an OUT for the transfer's purposes + * (devio.c:1690-1696). + */ + is_in = (setup[0] & 0x80) != 0 && wLength != 0; + data_len = wLength; + data_gva = uu.buffer + 8; + req.bmRequestType = setup[0]; + req.bRequest = setup[1]; + req.wValue = (UInt16) (setup[2] | (setup[3] << 8)); + req.wIndex = (UInt16) (setup[4] | (setup[5] << 8)); + req.wLength = wLength; + req.noDataTimeout = 0; /* usbfs URBs never time out */ + req.completionTimeout = 0; + if (!ep0_control) { + if (fi->pipe_type[pipe - 1] != kUSBControl) + return -LINUX_EINVAL; + int64_t rc = usbdev_ensure_iface_async(u, fi); + if (rc < 0) + return rc; + } else { + int64_t rc = usbdev_ensure_dev_async(u); + if (rc < 0) + return rc; + } + } else { + is_in = (uu.endpoint & 0x80) != 0; + uint8_t ptype = fi->pipe_type[pipe - 1]; + if (uu.type == LINUX_URB_TYPE_BULK) { + if (ptype != kUSBBulk && ptype != kUSBInterrupt) + return -LINUX_EINVAL; /* control/iso ep (devio.c:1715-1717) */ + } else { + if (ptype != kUSBInterrupt) + return -LINUX_EINVAL; + } + pipe_interrupt = ptype == kUSBInterrupt; + int64_t rc = usbdev_ensure_iface_async(u, fi); + if (rc < 0) + return rc; + data_len = (uint32_t) uu.buffer_length; + data_gva = uu.buffer; + mps = fi->pipe_mps[pipe - 1]; + } + + /* One process-wide byte budget, no URB-count cap (usbdev-urb.h). */ + size_t charge = data_len + sizeof(usbdev_urb_t); + if (!usbdev_memory_charge(charge)) + return -LINUX_ENOMEM; + pthread_mutex_lock(&u->async_lock); + u->nurbs++; + u->inflight_bytes += charge; + pthread_mutex_unlock(&u->async_lock); + + usbdev_urb_t *rec = calloc(1, sizeof(*rec)); + uint8_t *buf = NULL; + if (rec && data_len > 0) + buf = malloc(data_len); + if (!rec || (data_len > 0 && !buf)) { + free(buf); + free(rec); + pthread_mutex_lock(&u->async_lock); + u->nurbs--; + u->inflight_bytes -= charge; + pthread_mutex_unlock(&u->async_lock); + usbdev_memory_refund(charge); + return -LINUX_ENOMEM; + } + if (!is_in && data_len > 0 && guest_read(g, data_gva, buf, data_len) < 0) { + free(buf); + free(rec); + pthread_mutex_lock(&u->async_lock); + u->nurbs--; + u->inflight_bytes -= charge; + pthread_mutex_unlock(&u->async_lock); + usbdev_memory_refund(charge); + return -LINUX_EFAULT; + } + + urb_owner_store(rec, u); + rec->charge = charge; + rec->userurb = arg; + rec->data_gva = data_gva; + rec->type = uu.type; + rec->ep = uu.endpoint; + /* Both ep0 directions share the default control pipe: one FIFO key. */ + rec->ep_key = + (uu.type == LINUX_URB_TYPE_CONTROL && (uu.endpoint & 0x7f) == 0) + ? 0 + : uu.endpoint; + rec->pipe = pipe; + rec->intf = fi ? fi->intf : NULL; + rec->is_in = is_in; + + /* Direction-mismatched flags are ignored, not rejected (devio.c honors + * SHORT_NOT_OK for IN and ZERO_PACKET for OUT only, 1710-1737). + */ + rec->short_not_ok = is_in && (uu.flags & LINUX_URB_SHORT_NOT_OK) != 0; + rec->zero_packet = !is_in && (uu.flags & LINUX_URB_ZERO_PACKET) != 0; + rec->pipe_interrupt = pipe_interrupt; + rec->state = URB_QUEUED; + rec->data_len = data_len; + rec->mps = mps; + rec->buf = buf; + rec->req = req; + + pthread_mutex_lock(&u->async_lock); + rec->seq = ++u->urb_seq; + bool busy = u->ep_head[rec->ep_key] != NULL; + urb_pending_append(u, rec); + if (usbdev_ep_may_start(u->draining, u->ep_aborting[rec->ep_key], busy)) { + /* Endpoint idle: hand it to IOKit now. Queued follow-ups start from the + * completion callback (one in-flight URB per endpoint keeps DISCARDURB + * per-URB, see file header). + */ + IOReturn ir = usbdev_urb_start(rec); + if (ir != kIOReturnSuccess) { + urb_pending_unlink(u, rec); + urb_free_locked(u, rec); + pthread_mutex_unlock(&u->async_lock); + if (usbdev_ioret_device_gone(ir)) + usbdev_mark_disconnected(u); + + /* proc_do_submiturb has no -EINTR arm: an abort racing the start is + * a canceled transfer, not an interrupted syscall, and + * ioret_neg_errno's Aborted row is the syscall map. + */ + int64_t e = (uint32_t) ir == (uint32_t) kIOReturnAborted + ? -LINUX_EPROTO + : ioret_neg_errno(ir); + return e < 0 ? e : -LINUX_EPROTO; + } + rec->state = URB_INFLIGHT; + u->inflight++; + } + pthread_mutex_unlock(&u->async_lock); + return 0; +} + +/* DISCARDURB (proc_unlinkurb): arg is the user URB pointer. Entry lock held. A + * QUEUED record completes locally as killed (-ENOENT); an INFLIGHT one is + * flagged and aborted. + * + * Two things IOKit does not give for free. AbortPipe cancels everything + * outstanding on the pipe, not one transfer, so the endpoint's FIFO is shut for + * the duration of the abort (ep_aborting) -- without that the target could + * complete normally inside the unlock window, the callback would start the + * queued follower, and the abort would land on the follower instead: measured + * at a few per hundred thousand naturally, and reproducibly with the window + * widened, as the discarded URB reporting success and its innocent successor + * -ECONNRESET. And AbortPipe is asynchronous where the kernel call on this path + * is synchronous, so this waits for the record to leave the pending list before + * returning, which is what makes a REAPURBNDELAY issued straight after the + * discard find the URB the way it does on Linux. + * + * Which kernel call that is has been read the wrong way round twice, so it is + * written out here rather than left to the name: proc_unlinkurb calls + * usb_kill_urb, NOT usb_unlink_urb. usb_unlink_urb is the asynchronous one + * ("Success is indicated by returning -EINPROGRESS, at which time the URB will + * probably not yet have been given back"); usb_kill_urb is the one that waits + * ("It is guaranteed that upon return all completion handlers will have + * finished and the URB will be totally idle"), and it is the one DISCARDURB + * reaches. So waiting here is what matches Linux and returning early is what + * would diverge from it -- a guest that cancels and then reaps gets its URB on + * both, and libusb_cancel_transfer blocks for the transfer's lifetime on Linux + * too. + * + * The deliberate divergence is the bound, not the wait: usb_kill_urb never + * gives up, and this gives up after 2 s, logs, and leaves the record flagged + * and still reapable when its completion arrives. Trading an unbounded stall of + * a vCPU thread against a wire that may never answer is the same call + * usbdev_drain_for_change makes, and it is the discard-latency XFAIL printed by + * tests/test-usbdev-ioctl.c plus a bullet in the deviations list under USB + * Device Passthrough in docs/internals.md, which is prose there and not a row + * of the Deviations From Linux table below it. + */ +static int64_t usbdev_do_discardurb(usbdev_t *u, uint64_t arg) +{ + pthread_mutex_lock(&u->async_lock); + usbdev_urb_t *rec = u->pending_head; + while (rec && (rec->userurb != arg || rec->discarding)) + rec = rec->next; + if (!rec) { + pthread_mutex_unlock(&u->async_lock); + + /* Not this fd's URB, and possibly not this fd's device either. The scan + * above reads the pending list and nothing else, which is this layer's + * own bookkeeping and stays true after the device leaves, so a departed + * device answered -EINVAL with the fd left unstamped where + * proc_unlinkurb sits behind usbdev_do_ioctl's connected() gate and + * Linux answers -ENODEV. libusb_cancel_transfer hands its return + * straight to the application, so an unplug arrived there as + * LIBUSB_ERROR_NOT_FOUND -- "the transfer is already complete" -- and + * not as LIBUSB_ERROR_NO_DEVICE. Row DISCARDURB. + * + * Only on this arm: a discard that finds its record goes on to + * AbortPipe, which puts the question to the device itself, so the + * cancel a guest actually issues pays nothing for this. + */ + int64_t drc = usbdev_ensure_dev_reachable(u); + return drc < 0 ? drc : -LINUX_EINVAL; /* not pending */ + } + if (rec->state == URB_QUEUED) { + urb_pending_unlink(u, rec); + urb_complete_locked(u, rec, -LINUX_ENOENT); + pthread_mutex_unlock(&u->async_lock); + return 0; + } + rec->discarding = true; + uint8_t pipe = rec->pipe; + uint8_t key = rec->ep_key; + uint64_t seq = rec->seq; + IOUSBInterfaceInterface800 **intf = rec->intf; + u->ep_aborting[key]++; + pthread_mutex_unlock(&u->async_lock); + + /* The interface handle cannot be released concurrently: release paths need + * the entry lock this thread holds. + */ + IOReturn ar = pipe == 0 ? (*u->dev)->USBDeviceAbortPipeZero(u->dev) + : (*intf)->AbortPipe(intf, pipe); + + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += 2; + pthread_mutex_lock(&u->async_lock); + if (u->ep_aborting[key] > 0) + u->ep_aborting[key]--; + + /* An abort that answers kIOReturnNoDevice aborted nothing, and both of + * these used to discard it: the record stayed URB_INFLIGHT in pending, the + * fd was never stamped, and the wait below sat out its whole two seconds + * before returning 0 to a guest whose following REAPURB then never + * returned. Measured with a device-gone abort: DISCARDURB rc=0 after 2006 + * ms, fd unstamped, REAPURB killed at 20 s. + * + * Two things are owed. The stamp, because usbdev_arm_disconnect_watch names + * kIOReturnNoDevice detection on ops as its fallback and this is such an op + * -- the same reason usbdev_kick_ep_locked stamps when a start answers it. + * And the URB, because proc_unlinkurb is usb_kill_urb, which always leaves + * the URB reapable; a cancel that hands nothing back is the one thing this + * path may not do. + * + * It is handed back as an orphan rather than completed outright, which is + * the same answer the drain deadline gives a record it has given up on and + * for the same reason: the refusal says the user client has no device + * behind it, not that IOKit has forgotten the transfer, so the record is + * reapable at once while the buffer and the handles it references stay + * pinned until a callback that may never come. Completing it outright would + * let the reap free a record IOKit still holds the address of. + */ + if (usbdev_ioret_device_gone(ar)) { + usbdev_mark_disconnected_locked(u); + for (usbdev_urb_t *r = u->pending_head; r; r = r->next) { + if (r->seq != seq) + continue; + if (r->state == URB_INFLIGHT) + urb_orphan_locked(u, r); + break; + } + pthread_cond_broadcast(&u->async_cv); + } + for (;;) { + bool still = false; + for (usbdev_urb_t *r = u->pending_head; r; r = r->next) { + if (r->seq == seq) { + still = true; + break; + } + } + if (!still) + break; + + /* Bounded, because a wire that never answers must not park the vCPU + * thread forever: the record stays flagged and its completion is still + * reapable when it arrives. + */ + if (pthread_cond_timedwait(&u->async_cv, &u->async_lock, &deadline) == + ETIMEDOUT) { + log_warn("usbdev: DISCARDURB abort did not settle in 2s"); + break; + } + } + usbdev_kick_ep_locked(u, key); + pthread_mutex_unlock(&u->async_lock); + return 0; +} + +/* Copy one completion back to the guest (vCPU thread): IN data into the urb's + * buffer, then status/actual_length/error_count into the guest urb, then the + * userurb pointer into *arg (processcompl, devio.c:2043-2079). + */ +static int64_t usbdev_reap_copyout(guest_t *g, usbdev_urb_t *rec, uint64_t arg) +{ + if (rec->is_in && rec->actual > 0 && + guest_write(g, rec->data_gva, rec->buf, + rec->actual < rec->data_len ? rec->actual : rec->data_len) < + 0) + return -LINUX_EFAULT; + int32_t st = rec->status; + int32_t act = (int32_t) rec->actual; + int32_t ec = 0; + if (guest_write_small(g, + rec->userurb + offsetof(linux_usbdevfs_urb_t, status), + &st, sizeof(st)) < 0 || + guest_write_small( + g, rec->userurb + offsetof(linux_usbdevfs_urb_t, actual_length), + &act, sizeof(act)) < 0 || + guest_write_small( + g, rec->userurb + offsetof(linux_usbdevfs_urb_t, error_count), &ec, + sizeof(ec)) < 0) + return -LINUX_EFAULT; + if (guest_write_small(g, arg, &rec->userurb, sizeof(rec->userurb)) < 0) + return -LINUX_EFAULT; + return 0; +} + +/* Between the fd-table window a reap pass opens and the side-table lookup that + * has to answer for it, off unless ELFUSE_USBDEV_REAP_DELAY_US is set. The + * window is a few instructions wide unaided; what lives in it is a sibling's + * close and reopen of the fd number, which used to give this pass an entry from + * the new description and a readiness pipe from the old one. + */ +static void usbdev_reap_window_delay(void) +{ + static _Atomic long cached = -1; + usbdev_window_delay("ELFUSE_USBDEV_REAP_DELAY_US", &cached); +} + +/* REAPURB / REAPURBNDELAY. Called WITHOUT the entry lock held so a blocked reap + * never stalls submits or discards; each pass revalidates the fd. Blocking + * follows reap_as (devio.c:2081-2102) and the proc_reapurb that calls it + * (devio.c:2104-2119): reap_as wakes on completion or disconnect and returns + * what it found, and proc_reapurb turns an empty return into -EINTR without + * restart on a signal and -ENODEV once disconnected and drained + * (REAP_AFTER_DISCONNECT). + */ +static int64_t usbdev_do_reap(guest_t *g, int fd, uint64_t arg, bool block) +{ + for (;;) { + /* The slot's record and the descriptor behind it, out of ONE fd_lock + * window. They are two facts about one open file description: the + * readiness settled below is settled on this fd's pipe, and the wait + * parks on it. Read separately -- a bare fd_snapshot here and another + * inside usbdev_acquire, each proving the generation against its own + * window -- a sibling's close and reopen in between answered from the + * new description's entry while still holding the old one's pipe + * number, so the reap settled readiness against, and waited on, + * whatever had taken that number. Same shape and the same fix as + * sys_fstatfs (syscall/fs-stat.c); the pin also replaces the dup this + * used to take for the wait. + */ + fd_entry_t snap; + host_fd_ref_t ready; + int64_t rerr = host_fd_ref_open_entry(fd, &ready, &snap); + if (rerr < 0) + return rerr; + if (snap.type != FD_USBDEV) { + host_fd_ref_close(&ready); + return -LINUX_EBADF; + } + usbdev_reap_window_delay(); + usbdev_t *u = usbdev_acquire_snap(fd, &snap); + if (!u) { + host_fd_ref_close(&ready); + return -LINUX_EBADF; + } + pthread_mutex_lock(&u->async_lock); + usbdev_urb_t *rec = u->completed_head; + if (rec) { + u->completed_head = rec->next; + if (!u->completed_head) + u->completed_tail = NULL; + } + bool disc = u->disconnected; + bool pending = u->pending_head != NULL; + + /* REAP_AFTER_DISCONNECT, as one invariant: after a disconnect, every + * URB still in flight is handed back before any reap answers -ENODEV, + * whichever reap flavor got here first. That is what the capability bit + * promises, and -ENODEV is therefore decided by "nothing is left out" + * -- the pending list -- and never by a flag recording that some + * earlier pass did some work. + * + * Linux gets the invariant for free: usbdev_remove runs + * destroy_all_async -- a synchronous usb_kill_urb per URB -- before it + * wakes the reapers, so by the time any reap can observe the disconnect + * the pending list is already empty and -ENODEV cannot be premature. + * IOKit delivers nothing of its own when the device terminates + * (measured: three URBs outstanding at terminate, zero callbacks in the + * next three seconds, and the same URBs recovered in 1 ms by close()'s + * AbortPipe), so this engine has to issue that kill itself, and the + * aborts it issues land asynchronously. The window Linux does not have + * -- disconnected, aborts issued, URBs not back yet -- is therefore + * real here, and the answer inside it is -EAGAIN for a non-blocking + * reap and a wait for a blocking one. Neither is -ENODEV, because the + * URBs are still coming. + * + * Two separate decisions, which is the bug this shape exists to keep + * fixed. disc_drained is a one-shot for ISSUING the aborts, so they go + * out once; it is not permission to report the device drained. When it + * was both, a REAPURBNDELAY -- which issues aborts and by contract + * cannot wait for them -- latched it and every later reap, blocking + * included, then answered -ENODEV with the URBs still in flight: an + * ordinary libusb event loop polls non-blocking first, so that was the + * common path, not a corner. Measured with ep81:never plus terminate: + * REAPURBNDELAY rc=-19 urb=(nil), then REAPURB rc=-19 in 0 ms, with the + * URB only arriving 400 ms later. + */ + bool abort_now = !rec && disc && !u->disc_drained && pending; + if (abort_now) { + u->disc_drained = true; + u->disc_orphan_at_ms = usbdev_now_ms() + USBDEV_DRAIN_TIMEOUT_MS; + } + + /* A blocking pass drains whatever is still out, latch or no latch: the + * aborts are idempotent, and this is what makes the invariant hold for + * a blocking reap that arrives behind a non-blocking one. + */ + bool drain = abort_now || (!rec && disc && pending && block); + + /* The other half of that invariant: every reap flavor reaches the end + * of the conversation, and reaches it inside one drain deadline of the + * aborts. Neither flavor may answer -ENODEV while a URB may still come + * back, and neither may go on refusing to answer once no URB can. + * + * The blocking flavor gets the second half from the wait it takes + * below, which times out and orphans. A non-blocking one may not wait + * for anything, so it measures the same deadline instead of waiting it + * out: the aborts went out at disc_orphan_at_ms minus one deadline, and + * past it the wire has had exactly as long to answer them as a blocking + * reap would have given it. Without this the only thing that empties + * pending is the blocking arm's wait, so a wire that answers no abort + * left a REAPURBNDELAY loop returning -EAGAIN for ever while poll held + * POLLERR|POLLHUP -- measured against ep81:wedge(600000) as 1958386 + * -EAGAIN and no -ENODEV in 4 s, where Linux's proc_reapurbnonblock + * answers connected(ps) ? -EAGAIN : -ENODEV and the guest tears down. + */ + bool overdue = !rec && disc && pending && !block && u->disc_drained && + usbdev_now_ms() >= u->disc_orphan_at_ms; + if (overdue) { + usbdev_orphan_stalled_locked(u, NULL); + pending = u->pending_head != NULL; + + /* Whatever it handed back is reapable now, and this pass is the one + * that has to see it: rec was read before the orphaning, so leaving + * it alone answered -ENODEV below on the same pass that completed + * the URB, with the pointer never returned. Measured with + * ep02:wedge(600000) plus a terminate: REAPURBNDELAY -> -ENODEV + * after 2037 ms, URBs handed back 0. + */ + rec = u->completed_head; + if (rec) { + u->completed_head = rec->next; + if (!u->completed_head) + u->completed_tail = NULL; + } + } + + /* Settle the readiness level under the same lock that dequeued: the + * emptiness this reads is the emptiness it acted on, where a decision + * taken outside the lock would race a completion arriving behind it and + * strand the token it just wrote. + */ + usbdev_ready_settle_locked(u, ready.fd); + pthread_mutex_unlock(&u->async_lock); + if (rec) { + int64_t ret = usbdev_reap_copyout(g, rec, arg); + pthread_mutex_lock(&u->async_lock); + + /* An orphan handed back by usbdev_orphan_stalled_locked settled the + * slot's counters when the deadline gave up on it and may still be + * owned by an in-flight IOKit transfer, so it is not this slot's to + * account for or, while that is true, to free. + */ + if (rec->handback) + urb_handback_taken_locked(rec); + else + urb_free_locked(u, rec); + pthread_mutex_unlock(&u->async_lock); + usbdev_release(u); + host_fd_ref_close(&ready); + return ret; + } + if (drain) { + /* A blocking reap may wait for the callbacks the aborts provoke, + * and does: it loops and hands back what they retire. A + * REAPURBNDELAY may not wait for anything at all, so it issues the + * same aborts and lets a later pass collect them -- see + * usbdev_abort_urbs_nowait_locked. Either way the loop continues, + * so a record the aborts retired immediately (a QUEUED one) is + * handed back rather than reported as nothing left. + * + * Both are bounded by the same drain deadline and both orphan what + * misses it, which empties the pending list: that is what stops a + * wire which answers no abort from parking a blocking REAPURB or + * spinning a non-blocking one for good, and it is why the -ENODEV + * below is reachable at all on such a wire. The blocking arm spends + * the deadline in usbdev_kill_urbs_locked's wait; the non-blocking + * arm has already spent it above, having measured it rather than + * waited. + */ + if (block) + (void) usbdev_kill_urbs_locked(u, NULL); + else + usbdev_abort_urbs_nowait_locked(u); + usbdev_release(u); + host_fd_ref_close(&ready); + continue; /* hand the drained completions out on the next pass */ + } + usbdev_release(u); + + /* Drained is the whole of the -ENODEV condition. A disconnected fd with + * URBs still out has not finished answering for them yet, so it owes + * -EAGAIN (proc_reapurbnonblock's other arm) rather than the end of the + * conversation. Neither flavor can sit in that state past the drain + * deadline: the blocking one takes the drain arm above and the + * non-blocking one has orphaned what the deadline gave up on, so + * pending is empty here in both cases and -ENODEV is due. + */ + if (disc && !pending) { + host_fd_ref_close(&ready); + return -LINUX_ENODEV; + } + if (!block) { + host_fd_ref_close(&ready); + return -LINUX_EAGAIN; + } + + /* The pin is what makes waiting on this number safe: a sibling's + * close() after the entry lock is dropped frees the guest fd, and a + * wait on the raw host number would park on whatever object took it. + * The loop revalidates the guest fd after the wake. + */ + int64_t rc = io_wait_fd_or_interrupted(ready.fd, POLLIN); + host_fd_ref_close(&ready); + if (rc < 0) { + /* proc_reapurb returns -EINTR with no restart (devio.c:2116-2117); + * reap_as itself only breaks out of the wait and returns a pointer. + */ + syscall_restart_forbid(); + return rc; + } + } +} + +/* poll/select/epoll remap helpers (see poll.c call sites) */ + +bool usbdev_fd_disconnected(int guest_fd) +{ + return RANGE_CHECK(guest_fd, 0, FD_TABLE_SIZE) && discmap_load(guest_fd); +} + +bool usbdev_fd_reapable(int guest_fd) +{ + return RANGE_CHECK(guest_fd, 0, FD_TABLE_SIZE) && readymap_load(guest_fd); +} + +bool usbdev_poll_host_events(int guest_fd, + short guest_events, + short *host_events) +{ + fd_entry_t snap; + if (!fd_snapshot(guest_fd, &snap) || snap.type != FD_USBDEV) + return false; + + /* The host interest is always POLLIN on the completion pipe: the fd never + * carries guest-readable bytes, and a disconnect mid-wait wakes the parked + * poll through the readiness level usbdev_mark_disconnected raises -- and + * then holds, so a poll that arrives after the disconnect is woken by the + * same token rather than by a byte of its own -- even when the guest asked + * for nothing the pipe can signal (POLLERR|POLLHUP are unmaskable, + * devio.c:2842-2845). usbdev_poll_guest_revents filters what the guest + * actually sees, and the callers re-block when a wake maps to nothing + * guest-visible. + */ + (void) guest_events; + *host_events = POLLIN; + return true; +} + +short usbdev_poll_guest_revents(int guest_fd, + short guest_events, + short host_revents) +{ + /* usbdev_poll gates EPOLLOUT|EPOLLWRNORM on FMODE_WRITE (devio.c:2840), so + * this is the same capability the ioctl and write gates read, not a test + * against O_RDONLY: access mode 3 is not O_RDONLY and carries no + * FMODE_WRITE either, and testing the literal reported the fd writable + * where Linux does not. + */ + fd_entry_t snap; + bool writable = fd_snapshot(guest_fd, &snap) && snap.type == FD_USBDEV && + (usbdev_fmode(snap.linux_flags) & USBDEV_FMODE_WRITE); + short out = 0; + if (usbdev_fd_disconnected(guest_fd)) + out |= LINUX_POLLERR | LINUX_POLLHUP; /* devio.c:2842-2845 */ + /* POLLOUT|POLLWRNORM only while a completion is actually reapable + * (async_completed non-empty, devio.c poll): the pipe also carries the + * disconnect wake, and a disconnect with nothing left to reap must report + * ERR|HUP alone. + */ + if (writable && (host_revents & POLLIN) && usbdev_fd_reapable(guest_fd)) + out |= LINUX_POLLOUT | LINUX_POLLWRNORM; + /* do_pollfd masks by demanded events plus the unmaskable bits. */ + return (short) (out & (guest_events | LINUX_POLLERR | LINUX_POLLHUP | + LINUX_POLLNVAL)); +} + /* Registry 'Device Speed' -> USB_SPEED_* enum (ch9.h:1217-1222): the ioctl's * return value, not an out parameter. */ @@ -2120,33 +4761,130 @@ static int64_t usbdev_speed_enum(unsigned code) } } +/* The requests do_vfs_ioctl answers for every file before it calls + * f_op->unlocked_ioctl, so on Linux they never reach usbfs, never meet its + * FMODE_WRITE gate and never meet its connected() gate. io.c answers FIONBIO + * and FIOASYNC ahead of this file for the same reason, and this is the rest of + * that set. + * + * This layer models none of the ten and answers -ENOTTY to all of them. For + * eight that is not what Linux answers: each of those arms is reached there and + * answers for itself, off the superblock the node sits on, off CAP_SYS_ADMIN, + * or off the argument. Which two agree and what the other eight give instead is + * measured rather than reasoned from the identifier, and is recorded beside the + * -ENOTTY in check_vfs_ioctls, tests/test-usbdev-ioctl.c: that lane drives all + * ten on a read-only and on a writable fd and prints the eight as XFAIL. No + * sentence here repeats those values, because the last one that tried got two + * of them wrong. + * + * What is not in it: FIONREAD, which do_vfs_ioctl hands to vfs_ioctl for + * anything that is not a regular file, so it does reach usbfs; and the + * FS_IOC_*FLAGS and FS_IOC_FS*XATTR arms, which answer -ENOIOCTLCMD for an + * inode carrying no fileattr operations and are retried through vfs_ioctl. Both + * meet the gate below, and should. + */ +static bool usbdev_vfs_answers_first(uint32_t request) +{ + switch (request) { + case LINUX_FIOQSIZE: + case LINUX_FIGETBSZ: + case LINUX_FIFREEZE: + case LINUX_FITHAW: + case LINUX_FS_IOC_FIEMAP: + case LINUX_FICLONE: + case LINUX_FICLONERANGE: + case LINUX_FIDEDUPERANGE: + case LINUX_FS_IOC_GETFSUUID: + case LINUX_FS_IOC_GETFSSYSFSPATH: + return true; + default: + return false; + } +} + int64_t usbdev_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg) { fd_entry_t snap; if (!fd_snapshot(fd, &snap) || snap.type != FD_USBDEV) return -LINUX_EBADF; - /* Every usbdev ioctl needs FMODE_WRITE (devio.c:2605-2606). */ + + /* Ahead of everything below, because Linux never puts any of it to these: + * they are answered before f_op->unlocked_ioctl runs, so neither the + * FMODE_WRITE gate nor the device question is theirs to meet. Two answers + * move because this sits here and not lower down: on a device that is + * present a read-only fd now gets -ENOTTY where the gate below had made it + * -EPERM, and on a device that has gone a writable fd gets -ENOTTY where + * that gate and the ask in the default arm had both made it -ENODEV. + * check_vfs_ioctls in tests/test-usbdev-ioctl.c drives the first on both + * access modes and tests/test-usbdev-ioctl-departed.c the second, so this + * moving below the gate fails a lane rather than passing quietly. + */ + if (usbdev_vfs_answers_first((uint32_t) request)) + return -LINUX_ENOTTY; + + /* Every usbdev ioctl needs FMODE_WRITE (devio.c:2608-2609). */ if (!(usbdev_fmode(snap.linux_flags) & USBDEV_FMODE_WRITE)) return -LINUX_EPERM; + /* The reaps manage their own locking: a blocked REAPURB must not hold the + * entry lock against concurrent SUBMITURB/DISCARDURB, and they stay usable + * after disconnect (devio.c:2613-2636). + */ + if ((uint32_t) request == USBDEVFS_REAPURB || + (uint32_t) request == USBDEVFS_REAPURBNDELAY) + return usbdev_do_reap(g, fd, arg, + (uint32_t) request == USBDEVFS_REAPURB); + usbdev_t *u = usbdev_acquire(fd); if (!u) return -LINUX_EBADF; + /* usbdev_ioctl's connected() gate. What it reads is what this fd has been + * told: the mark arrives from the terminate watch, from a peer's walk when + * some other fd on the node was told first, or from this fd's own next call + * into IOKit. open(2) resolves no service (usbdev_ensure_service says why), + * so an fd opened after the device left starts every call with nothing on + * it, and what closes that window is the op itself putting the question to + * the device. + * + * Not every op does, and this comment does not say which. That count was + * written down wrong twice. What each arm below answers on a device that + * has gone, what Linux answers, and which differences are deliberate is + * tests/usbdev-ioctl-departed.tbl: one row per request, joined against this + * dispatch by scripts/gen-usbdev-ioctl-departed.py so an arm added here + * cannot escape the table, and driven by + * tests/test-usbdev-ioctl-departed.c, which fails both when a recorded gap + * widens and when one quietly closes. Running that lane prints the table. + * + * The default arm is in that join too, through the request codes defined at + * the top of this file and dispatched nowhere, because a sentence asserting + * over every arm that cannot be checked against the arm that catches the + * rest is how the -ENOTTY that arm used to answer here survived a table + * built to find exactly that. + */ + bool disc; + pthread_mutex_lock(&u->async_lock); + disc = u->disconnected; + pthread_mutex_unlock(&u->async_lock); + if (disc) { + usbdev_release(u); + return -LINUX_ENODEV; + } + int64_t ret; switch ((uint32_t) request) { case USBDEVFS_CLAIMINTERFACE: { uint32_t ifnum; ret = guest_read_small(g, arg, &ifnum, sizeof(ifnum)) < 0 ? -LINUX_EFAULT - : usbdev_claim_locked(u, ifnum); + : usbdev_claim_ioctl(u, ifnum); break; } case USBDEVFS_RELEASEINTERFACE: { uint32_t ifnum; ret = guest_read_small(g, arg, &ifnum, sizeof(ifnum)) < 0 ? -LINUX_EFAULT - : usbdev_release_locked(u, ifnum); + : usbdev_release_ioctl(u, ifnum); break; } case USBDEVFS_SETINTERFACE: @@ -2190,17 +4928,69 @@ int64_t usbdev_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg) break; case USBDEVFS_RESET: { /* Stage-2 deviation (see file header): clear stalls on every claimed - * pipe instead of re-enumerating, and report success. + * pipe instead of re-enumerating. + * + * usb_reset_device kills every URB on the device first, so the guest + * gets all of them back. Doing the stall clears alone left the URB + * behind whichever pipe IOKit happened to abort reported as canceled + * and the queue behind it stranded -- one of three returned in a + * measured run. + * + * A per-pipe clear that fails is logged, not returned: the stall clears + * are the substitute, not the operation the guest asked for, and this + * board answers kIOUSBTransactionTimeout on the CDC data interface's + * pipes for a device usb_reset_device would reset without complaint. + * Reporting that would refuse a RESET Linux performs, which is a worse + * answer than the silence. + * + * The one caller that does not go through usbdev_drain_for_change, and + * deliberately: a drain that misses its deadline means a transfer is + * wedged, which is the state a guest issues RESET to get out of, and + * usb_reset_device is a recovery path that never refuses for that + * reason. Nothing here retires a handle an orphan holds either -- a + * stall clear leaves the pipeRefs where they are, unlike SETINTERFACE + * and SETCONFIGURATION -- so proceeding costs the orphans nothing. + */ + + /* The device question first, and unconditionally. The only IOKit calls + * this op makes are the aborts inside the kill below and the stall + * clears after it, and it makes neither with nothing claimed and + * nothing in flight, so RESET on a departed device ran to the end and + * answered 0 -- measured on a fresh fd of a terminated loopback device, + * rc=0 with revents 0x0. libusb_reset_device is the canonical recovery + * call, so the guest's NO_DEVICE branch was the one branch an unplug + * could not reach. Linux answers -ENODEV: connected() runs before + * proc_resetdevice. */ + int64_t rrc = usbdev_ensure_dev_reachable(u); + if (rrc < 0) { + ret = rrc; + break; + } + (void) usbdev_kill_urbs_locked(u, NULL); for (int i = 0; i < USBDEV_MAX_IFACES; i++) { usbdev_iface_t *fi = &u->ifaces[i]; if (!fi->claimed) continue; - for (int p = 1; p <= fi->npipes; p++) - (void) (*fi->intf)->ClearPipeStallBothEnds(fi->intf, (UInt8) p); + for (int p = 1; p <= fi->npipes; p++) { + IOReturn r = + (*fi->intf)->ClearPipeStallBothEnds(fi->intf, (UInt8) p); + if (r != kIOReturnSuccess) { + /* The clear is logged rather than returned, but its status + * is still translated, so a device that leaves between the + * gate above and this loop is still originated here rather + * than only on the next op. The gate is what carries the + * -ENODEV answer; this carries the stamp for the window + * after it. + */ + (void) usbdev_ioret_op(u, r); + log_warn("usbdev: RESET: pipe %d stall clear -> 0x%x", p, + r); + } + } } log_debug( - "usbdev: RESET emulated as pipe-stall clear (no " + "usbdev: RESET emulated as URB kill + pipe-stall clear (no " "re-enumeration)"); ret = 0; break; @@ -2212,19 +5002,54 @@ int64_t usbdev_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg) ret = usbdev_do_driver_ioctl(u, g, arg); break; case USBDEVFS_SUBMITURB: - log_warn("usbdev: SUBMITURB not implemented (stage 3)"); - ret = -LINUX_ENOTTY; + ret = usbdev_do_submiturb(u, g, arg); break; case USBDEVFS_DISCARDURB: - case USBDEVFS_REAPURB: - case USBDEVFS_REAPURBNDELAY: - case USBDEVFS_DISCSIGNAL: - log_debug("usbdev: async URB ioctl 0x%llx not implemented (stage 3)", - (unsigned long long) request); - ret = -LINUX_ENOTTY; + ret = usbdev_do_discardurb(u, arg); + break; + case USBDEVFS_DISCSIGNAL: { + /* Stored for fidelity but never delivered (file header): elfuse has no + * async guest-signal injection from the event thread. + */ + linux_usbdevfs_disconnectsignal_t ds; + if (guest_read_small(g, arg, &ds, sizeof(ds)) < 0) { + ret = -LINUX_EFAULT; + } else { + u->discsig_signr = ds.signr; + u->discsig_context = ds.context; + if (ds.signr) + log_warn( + "usbdev: DISCSIGNAL %u accepted but will not be " + "delivered", + ds.signr); + ret = 0; + } break; + } default: - ret = -LINUX_ENOTTY; + /* The device question outranks the unknown request, because Linux's + * connected() gate runs ahead of usbdev_do_ioctl's switch + * (devio.c:2638) and the -ENOTTY is that switch's default. So a + * usbdevfs cmd this layer does not serve answered -ENOTTY and stamped + * nothing on a departed device, where Linux answers -ENODEV with + * POLLERR|POLLHUP: measured on a terminated loopback device, + * HUB_PORTINFO, ALLOC_STREAMS and WAIT_FOR_RESUME were all -1/ENOTTY + * with revents 0x0 on the asking fd and on a peer. libusb reads the + * difference -- ENOTTY on ALLOC_STREAMS is NOT_SUPPORTED, ENODEV is + * NO_DEVICE -- so an unplug read as "this kernel has no streams" and + * never expired. + * + * The gate at the top of this function is not enough for it: what that + * reads is what this fd has been told, and an arm that reaches no wire + * never tells it anything. This arm asks. + * + * It asks only for requests that would have reached usbfs. + * usbdev_vfs_answers_first takes the rest above the gate, because for + * those Linux never reaches the code that could ask. + */ + ret = usbdev_ensure_dev_reachable(u); + if (ret >= 0) + ret = -LINUX_ENOTTY; break; } usbdev_release(u); diff --git a/src/syscall/usbdev.h b/src/syscall/usbdev.h index 0429d403..ed4e42e7 100644 --- a/src/syscall/usbdev.h +++ b/src/syscall/usbdev.h @@ -4,14 +4,17 @@ * Copyright 2026 elfuse contributors * SPDX-License-Identifier: Apache-2.0 * - * Stage 2 of the usbdevfs emulation: a real FD_USBDEV fd type whose synchronous - * ioctls (CLAIMINTERFACE, CONTROL, BULK, ...) are served by - * IOUSBDeviceInterface/IOUSBInterfaceInterface plugins. Async URBs - * (SUBMITURB/REAPURB) are stage 3. + * Stages 2+3 of the usbdevfs emulation: a real FD_USBDEV fd type whose + * synchronous ioctls (CLAIMINTERFACE, CONTROL, BULK, ...) and async URBs + * (SUBMITURB/DISCARDURB/REAPURB*) are served by + * IOUSBDeviceInterface/IOUSBInterfaceInterface plugins plus one CFRunLoop + * completion thread. */ #pragma once +#include + #include #include @@ -72,3 +75,28 @@ int64_t usbdev_fstat(int fd, struct stat *st); * Returns -LINUX_* or the (possibly positive) ioctl result. */ int64_t usbdev_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg); + +/* poll/select/epoll remap: the usbfs fd signals guest POLLOUT|POLLWRNORM + * ("completed URBs reapable", devio.c:2833-2847) while its backing pipe's read + * end raises host POLLIN, and disconnect must surface as POLLERR|POLLHUP. + * usbdev_poll_host_events returns false when guest_fd is not FD_USBDEV; + * otherwise it yields the host-side events to poll the pipe with, and + * usbdev_poll_guest_revents converts the host result back into Linux poll bits + * (already masked by the demanded events). + */ +bool usbdev_poll_host_events(int guest_fd, + short guest_events, + short *host_events); +short usbdev_poll_guest_revents(int guest_fd, + short guest_events, + short host_revents); + +/* Lock-free "device gone" test for the epoll merge path. */ +bool usbdev_fd_disconnected(int guest_fd); + +/* Lock-free "a completion is reapable" test: guest-visible writability + * (POLLOUT/EPOLLOUT/select write set) exists only while the completed list is + * non-empty, so every readiness remap must pair the pipe's readability with + * this check (the pipe also carries the disconnect wake). + */ +bool usbdev_fd_reapable(int guest_fd); diff --git a/tests/test-epoll.c b/tests/test-epoll.c index 4209e001..c0c6d2fe 100644 --- a/tests/test-epoll.c +++ b/tests/test-epoll.c @@ -67,6 +67,192 @@ int main(void) close(epfd); } + /* The read half of "epoll reads the same bits poll does". eventpoll has no + * mask of its own: ep_item_poll masks the file's answer by + * epi->event.events, so a registration naming only EPOLLRDNORM asks for one + * of the two bits a readable pipe raises, has to be woken by it, and has to + * be told EPOLLRDNORM rather than EPOLLIN. Gating the arming on EPOLLIN + * alone registered no filter at all and the wait ran to its timeout. + */ + TEST("ADD pipe + wait EPOLLRDNORM alone"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLRDNORM, .data.fd = pipefd[0]}; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &ev) == 0) { + write(pipefd[1], "x", 1); + + struct epoll_event out = {0}; + int n = epoll_wait(epfd, &out, 1, 500); + EXPECT_TRUE(n == 1 && (out.events & EPOLLRDNORM) && + !(out.events & EPOLLIN) && out.data.fd == pipefd[0], + "EPOLLRDNORM alone did not fire as EPOLLRDNORM"); + } else + FAIL("epoll_ctl ADD failed"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + } + + /* The mask is reported back as asked for, both bits or one. */ + TEST("EPOLLIN|EPOLLRDNORM reports both"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLIN | EPOLLRDNORM, + .data.fd = pipefd[0]}; + struct epoll_event out = {0}; + int n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &ev) == 0) { + write(pipefd[1], "x", 1); + n = epoll_wait(epfd, &out, 1, 500); + } + EXPECT_TRUE(n == 1 && (out.events & (EPOLLIN | EPOLLRDNORM)) == + (EPOLLIN | EPOLLRDNORM), + "both bits asked for, both not reported"); + + TEST("and EPOLLIN alone reports only EPOLLIN"); + ev.events = EPOLLIN; + out.events = 0; + n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_MOD, pipefd[0], &ev) == 0) + n = epoll_wait(epfd, &out, 1, 500); + EXPECT_TRUE( + n == 1 && (out.events & EPOLLIN) && !(out.events & EPOLLRDNORM), + "EPOLLIN alone reported a bit it did not ask for"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + } + + /* A registration naming no read bit at all still has its read filter armed, + * to catch the EOF EPOLLRDHUP is about -- but armed with a low-water mark + * no readable byte can reach, so mere readability is not an event it hears. + * That is what Linux does: do_epoll_ctl widens the requested mask by + * EPOLLERR|EPOLLHUP only, ep_item_poll masks the pipe's EPOLLIN|EPOLLRDNORM + * by it, and nothing survives, so ep_poll waits the caller out and returns + * 0 at the deadline. Measured at rc=0 after 503-510 ms on Linux 6.18.50 + * (aarch64), the qemu reference lane this file also runs in. + * + * The answer neither side may give is 0 *before* the deadline, which would + * make a guest treat a timeout it never waited for as one it did. So the + * elapsed time is the assertion, not just the count. + */ + TEST("EPOLLRDHUP alone waits its timeout out on a merely readable fd"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLRDHUP, .data.fd = pipefd[0]}; + struct epoll_event out = {0}; + int n = -1; + struct timespec t0 = {0}, t1 = {0}; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &ev) == 0) { + write(pipefd[1], "x", 1); + clock_gettime(CLOCK_MONOTONIC, &t0); + n = epoll_wait(epfd, &out, 1, 500); + clock_gettime(CLOCK_MONOTONIC, &t1); + } + long waited_ms = (long) (t1.tv_sec - t0.tv_sec) * 1000 + + (t1.tv_nsec - t0.tv_nsec) / 1000000; + EXPECT_TRUE(n == 0 && waited_ms >= 450, + "a merely readable fd was reported to an EPOLLRDHUP-only " + "registration, or the wait ended before its timeout"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + } + + /* And the hangup it did ask for still arrives at once. This is the other + * half of the low-water mark above: silencing readability must not silence + * the EOF, which activates the read filter whatever the mark is. + */ + TEST("EPOLLRDHUP alone still reports the hangup when the writer closes"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLRDHUP, .data.fd = pipefd[0]}; + struct epoll_event out = {0}; + int n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &ev) == 0) { + write(pipefd[1], "x", 1); + close(pipefd[1]); + pipefd[1] = -1; + n = epoll_wait(epfd, &out, 1, 500); + } + + /* A pipe hangup is EPOLLHUP on Linux; this layer also sets EPOLLRDHUP + * on it, since kqueue reports one EV_EOF for both the full hangup a + * pipe means by it and the half-shutdown a socket does. Only the bit + * both agree on is asserted here. + */ + EXPECT_TRUE(n == 1 && (out.events & EPOLLHUP), + "a closed writer raised no hangup"); + + close(pipefd[0]); + if (pipefd[1] >= 0) + close(pipefd[1]); + close(epfd); + } + + /* MOD onto the read pair has to arm, and DEL of it has to disarm: both + * decide from the same mask the ADD above does. + */ + TEST("MOD to EPOLLRDNORM arms, DEL of it disarms"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLOUT, .data.fd = pipefd[0]}; + struct epoll_event out = {0}; + int n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &ev) == 0) { + ev.events = EPOLLRDNORM; + if (epoll_ctl(epfd, EPOLL_CTL_MOD, pipefd[0], &ev) == 0) { + write(pipefd[1], "x", 1); + n = epoll_wait(epfd, &out, 1, 500); + } + } + EXPECT_TRUE(n == 1 && (out.events & EPOLLRDNORM), + "MOD onto EPOLLRDNORM armed nothing"); + + TEST("and DEL of an EPOLLRDNORM-only registration is silent after"); + int d = epoll_ctl(epfd, EPOLL_CTL_DEL, pipefd[0], NULL); + out.events = 0; + n = epoll_wait(epfd, &out, 1, 100); + EXPECT_TRUE(d == 0 && n == 0, "DEL left the read filter registered"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + } + /* Test EPOLLOUT on pipe write end (always writable) */ TEST("ADD pipe write + EPOLLOUT"); { @@ -91,6 +277,106 @@ int main(void) close(epfd); } + /* The write half of the same rule, off the usbfs path. ep_item_poll masks + * the file's answer by epi->event.events, and a pipe with room raises + * EPOLLOUT|EPOLLWRNORM (fs/pipe.c:697 at v6.18), as a writable socket does + * (net/ipv4/tcp.c:606 at the same tag), so a registration naming only + * EPOLLWRNORM is a legal write registration Linux both wakes and reports as + * EPOLLWRNORM. Gating the arming on EPOLLOUT alone registered no + * EVFILT_WRITE and the wait ran to its timeout. + */ + TEST("ADD pipe write + wait EPOLLWRNORM alone"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLWRNORM, .data.fd = pipefd[1]}; + struct epoll_event out = {0}; + int n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[1], &ev) == 0) + n = epoll_wait(epfd, &out, 1, 500); + EXPECT_TRUE(n == 1 && (out.events & EPOLLWRNORM) && + !(out.events & EPOLLOUT) && out.data.fd == pipefd[1], + "EPOLLWRNORM alone did not fire as EPOLLWRNORM"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + } + + /* The write mask is reported back as asked for, both bits or one. */ + TEST("EPOLLOUT|EPOLLWRNORM reports both"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLOUT | EPOLLWRNORM, + .data.fd = pipefd[1]}; + struct epoll_event out = {0}; + int n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[1], &ev) == 0) + n = epoll_wait(epfd, &out, 1, 500); + EXPECT_TRUE(n == 1 && (out.events & (EPOLLOUT | EPOLLWRNORM)) == + (EPOLLOUT | EPOLLWRNORM), + "both write bits asked for, both not reported"); + + TEST("and EPOLLOUT alone reports only EPOLLOUT"); + ev.events = EPOLLOUT; + out.events = 0; + n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_MOD, pipefd[1], &ev) == 0) + n = epoll_wait(epfd, &out, 1, 500); + EXPECT_TRUE( + n == 1 && (out.events & EPOLLOUT) && !(out.events & EPOLLWRNORM), + "EPOLLOUT alone reported a bit it did not ask for"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + } + + /* MOD onto the write pair has to arm, and DEL of it has to disarm: both + * decide from the same mask the ADD above does, the way the read twins do. + */ + TEST("MOD to EPOLLWRNORM arms, DEL of it disarms"); + { + int epfd = epoll_create1(0); + int pipefd[2]; + if (pipe(pipefd) < 0) { + FAIL("pipe"); + pipefd[0] = pipefd[1] = -1; + } + + struct epoll_event ev = {.events = EPOLLIN, .data.fd = pipefd[1]}; + struct epoll_event out = {0}; + int n = -1; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[1], &ev) == 0) { + ev.events = EPOLLWRNORM; + if (epoll_ctl(epfd, EPOLL_CTL_MOD, pipefd[1], &ev) == 0) + n = epoll_wait(epfd, &out, 1, 500); + } + EXPECT_TRUE(n == 1 && (out.events & EPOLLWRNORM), + "MOD onto EPOLLWRNORM armed nothing"); + + TEST("and DEL of an EPOLLWRNORM-only registration is silent after"); + int d = epoll_ctl(epfd, EPOLL_CTL_DEL, pipefd[1], NULL); + out.events = 0; + n = epoll_wait(epfd, &out, 1, 100); + EXPECT_TRUE(d == 0 && n == 0, "DEL left the write filter registered"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + } + /* Test EPOLL_CTL_MOD: change monitored events */ TEST("CTL_MOD events"); { diff --git a/tests/test-usbdev-ioctl-departed.c b/tests/test-usbdev-ioctl-departed.c new file mode 100644 index 00000000..c1f2432f --- /dev/null +++ b/tests/test-usbdev-ioctl-departed.c @@ -0,0 +1,881 @@ +/* + * Every usbdevfs ioctl on a device that has gone (ELFUSE_USB_FIXTURE=loopback) + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Code under test: usbdev_ioctl's disconnect contract in src/syscall/usbdev.c + * and the usbdevfs arms of src/syscall/poll.c, across the whole ioctl surface + * rather than the handful of ops a reviewer thought to name. + * + * The surface is not listed here. scripts/gen-usbdev-ioctl-departed.py reads it + * out of usbdev_ioctl's own dispatch, joins it against the Linux answers + * recorded in tests/usbdev-ioctl-departed.tbl, and emits the rows below; an + * ioctl the layer implements with no recorded answer fails that generator, so + * this lane covers the surface by construction. Each row carries four columns + * and this binary asserts all four: the ioctl return, the errno behind it, the + * poll revents left on the fd that asked, and the revents on another fd open on + * the same node, which is what says the disconnect was recorded against the + * device rather than against one caller. + * + * A row whose recorded elfuse answer differs from Linux's is an XFAIL carrying + * both values: it must answer what this layer answers, and it must not answer + * what Linux does. So a gap cannot widen unnoticed and cannot close unnoticed + * either. + * + * Three of the rows name requests the layer defines and dispatches nowhere, so + * they drive usbdev_ioctl's default arm: the arm the join could not reach while + * it was built from case labels alone, and the one answering -ENOTTY where + * Linux answers -ENODEV. + * + * Every row asks on an fd that has not been told the device left. The fresh + * phase ends by asking on one that has, which is where the universal in + * docs/internals.md holds: the whole usbdevfs surface answers -ENODEV on a + * marked fd. It ends by asking where that universal stops, too -- the requests + * do_vfs_ioctl answers before f_op->unlocked_ioctl, which are not usbdevfs and + * are not -ENODEV on any fd of a departed device. + * + * The device is the loopback fixture's, /dev/bus/usb/003/001 interface 2, and + * it is terminated once at startup. Three things follow from that and shape the + * run: + * + * Every correct -ENODEV stamps every fd open on the node, so no two rows can + * share an fd. Each row opens its own subject and its own peer. + * + * A synchronous-only fd is never told the device left: the terminate watch is + * armed by the async paths alone. That is what leaves an unstamped fd for + * each row to ask on, and it is why the phases below can hold a claim across + * the terminate. + * + * The fixture's terminate is delivered by the event thread, which the same + * async paths start, so one throwaway fd submits one URB to bring the thread + * up and is closed again before the terminate lands. + * + * Phases, one process each (see mk/tests.mk): the rows that need a claim taken + * before the device left cannot share a process with the rows that stamp every + * fd on the node. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +/* Generated into build/ from tests/usbdev-ioctl-departed.tbl; see the Makefile + * rule that puts build/ on this binary's include path. + */ +#include "usbdev-ioctl-departed-vectors.h" + +int passes = 0, fails = 0; + +#define NODE "/dev/bus/usb/003/001" +#define IFNUM 2 +#define EP_IN 0x81 +#define EP_OUT 0x02 + +/* Past USBDEV_MAX_IFACES, so the layer's own interface-number bound is what + * would answer if the device question did not run first. + */ +#define IFNUM_UNREPRESENTABLE 200 + +#define USBDEVFS_CONTROL 0xc0185500u +#define USBDEVFS_IOCTL_DISCONNECT 0x5516 + +/* The fixture's control plane (src/syscall/usbdev-fixture.c). */ +#define FX_TERMINATE 0xf2 + +struct ctrltransfer { + uint8_t bRequestType, bRequest; + uint16_t wValue, wIndex, wLength; + uint32_t timeout; + void *data; +}; + +struct bulktransfer { + unsigned int ep, len, timeout; + void *data; +}; + +struct setinterface { + unsigned int interface, altsetting; +}; + +struct getdriver { + unsigned int interface; + char driver[256]; +}; + +struct disconnect_claim { + unsigned int interface, flags; + char driver[256]; +}; + +struct usbdevfs_ioctl { + int ifno, ioctl_code; + void *data; +}; + +struct connectinfo { + unsigned int devnum; + unsigned char slow; +}; + +struct disconnectsignal { + unsigned int signr; + void *context; +}; + +/* Arguments of the three requests this layer defines and does not dispatch. */ +struct hub_portinfo { + char nports; + char port[127]; +}; + +struct streams { + unsigned int num_streams, num_eps; + unsigned char eps[4]; +}; + +struct urb { + unsigned char type, endpoint; + int status; + unsigned int flags; + void *buffer; + int buffer_length, actual_length, start_frame; + union { + int number_of_packets; + unsigned int stream_id; + } u; + int error_count; + unsigned int signr; + void *usercontext; +}; + +#define URB_TYPE_CONTROL 2 +#define URB_TYPE_BULK 3 + +/* No type accepts this bit, so proc_do_submiturb's argument gate is what would + * answer if the device question did not run first (devio.c:1644-1650). + */ +#define URB_FLAG_UNDEFINED 0x08u + +static const char *stage = "startup"; + +static void on_alarm(int sig) +{ + (void) sig; + static char pre[] = "\nTIMEOUT in stage: "; + (void) !write(1, pre, sizeof(pre) - 1); + (void) !write(1, stage, strlen(stage)); + (void) !write(1, "\n", 1); + _exit(1); +} + +static void enter(const char *name) +{ + stage = name; + alarm(30); +} + +static char msgbuf[512]; +#define FAILF(...) \ + do { \ + snprintf(msgbuf, sizeof(msgbuf), __VA_ARGS__); \ + FAIL(msgbuf); \ + } while (0) +#define CHECK(cond, ...) \ + do { \ + if (cond) \ + PASS(); \ + else \ + FAILF(__VA_ARGS__); \ + } while (0) + +static void settle(int ms) +{ + (void) poll(NULL, 0, ms); +} + +static int revents_of(int f) +{ + struct pollfd p = {.fd = f, .events = POLLIN | POLLOUT}; + return poll(&p, 1, 0) > 0 ? p.revents : 0; +} + +/* the row drivers, one per generated row */ + +static long departed_drive_CONTROL(int f, unsigned long req) +{ + static uint8_t buf[8]; + struct ctrltransfer ct = {.bRequestType = 0xc0, /* vendor IN: no recipient + check ahead of the wire + */ + .bRequest = 0x01, + .wLength = sizeof(buf), + .timeout = 1000, + .data = buf}; + return ioctl(f, req, &ct); +} + +static long departed_drive_BULK(int f, unsigned long req) +{ + static uint8_t buf[8]; + struct bulktransfer bt = { + .ep = EP_IN, .len = sizeof(buf), .timeout = 1000, .data = buf}; + return ioctl(f, req, &bt); +} + +static long departed_drive_RESETEP(int f, unsigned long req) +{ + unsigned int ep = EP_IN; + return ioctl(f, req, &ep); +} + +static long departed_drive_CLEAR_HALT(int f, unsigned long req) +{ + unsigned int ep = EP_IN; + return ioctl(f, req, &ep); +} + +static long departed_drive_SETINTERFACE(int f, unsigned long req) +{ + struct setinterface si = {.interface = IFNUM, .altsetting = 0}; + return ioctl(f, req, &si); +} + +static long departed_drive_SETCONFIGURATION(int f, unsigned long req) +{ + unsigned int cfg = 1; + return ioctl(f, req, &cfg); +} + +static long departed_drive_GETDRIVER(int f, unsigned long req) +{ + static struct getdriver gd; + gd.interface = 0; + return ioctl(f, req, &gd); +} + +static long departed_drive_DISCONNECT_CLAIM(int f, unsigned long req) +{ + static struct disconnect_claim dc; + dc.interface = 0; + dc.flags = 0; + return ioctl(f, req, &dc); +} + +static long departed_drive_DRIVER_IOCTL(int f, unsigned long req) +{ + struct usbdevfs_ioctl ci = {.ifno = 0, + .ioctl_code = USBDEVFS_IOCTL_DISCONNECT}; + return ioctl(f, req, &ci); +} + +static long departed_drive_RESET(int f, unsigned long req) +{ + return ioctl(f, req, NULL); +} + +static long departed_drive_CLAIMINTERFACE(int f, unsigned long req) +{ + unsigned int ifn = 0; + return ioctl(f, req, &ifn); +} + +static long departed_drive_CLAIMINTERFACE_BOUND(int f, unsigned long req) +{ + unsigned int ifn = IFNUM_UNREPRESENTABLE; + return ioctl(f, req, &ifn); +} + +static long departed_drive_CLAIMINTERFACE_HELD(int f, unsigned long req) +{ + unsigned int ifn = IFNUM; + return ioctl(f, req, &ifn); +} + +static long departed_drive_RELEASEINTERFACE(int f, unsigned long req) +{ + unsigned int ifn = 0; + return ioctl(f, req, &ifn); +} + +static long departed_drive_RELEASEINTERFACE_BOUND(int f, unsigned long req) +{ + unsigned int ifn = IFNUM_UNREPRESENTABLE; + return ioctl(f, req, &ifn); +} + +static long departed_drive_RELEASEINTERFACE_HELD(int f, unsigned long req) +{ + unsigned int ifn = IFNUM; + return ioctl(f, req, &ifn); +} + +static long departed_drive_SUBMITURB(int f, unsigned long req) +{ + static uint8_t buf[8]; + static struct urb u; + memset(&u, 0, sizeof(u)); + u.type = URB_TYPE_BULK; + u.endpoint = EP_OUT; + u.buffer = buf; + u.buffer_length = sizeof(buf); + return ioctl(f, req, &u); +} + +static long departed_drive_SUBMITURB_BADFLAGS(int f, unsigned long req) +{ + static uint8_t buf[8]; + static struct urb u; + memset(&u, 0, sizeof(u)); + u.type = URB_TYPE_BULK; + u.endpoint = EP_OUT; + u.flags = URB_FLAG_UNDEFINED; + u.buffer = buf; + u.buffer_length = sizeof(buf); + return ioctl(f, req, &u); +} + +static long departed_drive_DISCARDURB(int f, unsigned long req) +{ + static struct urb never_submitted; + return ioctl(f, req, &never_submitted); +} + +/* A blocking reap has no bound of its own once the wait is entered, so this + * ends it with a signal rather than letting the lane hang: the interval timer + * repeats so a wait this layer refuses to leave still ends the run, and the + * lane's own watchdog handler is put back before the driver returns. + */ +static volatile sig_atomic_t reap_alarms; + +static void on_reap_alarm(int sig) +{ + (void) sig; + if (++reap_alarms < 5) + return; + static char msg[] = "\nREAPURB did not leave its wait on a signal\n"; + (void) !write(1, msg, sizeof(msg) - 1); + _exit(1); +} + +static long departed_drive_REAPURB(int f, unsigned long req) +{ + void *urbp = NULL; + struct sigaction sa, old; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = on_reap_alarm; /* no SA_RESTART: the wait must not resume */ + reap_alarms = 0; + alarm(0); + sigaction(SIGALRM, &sa, &old); + struct itimerval it = {.it_interval = {.tv_sec = 2}, + .it_value = {.tv_sec = 2}}; + setitimer(ITIMER_REAL, &it, NULL); + long r = ioctl(f, req, &urbp); + int e = errno; + struct itimerval off = {{0, 0}, {0, 0}}; + setitimer(ITIMER_REAL, &off, NULL); + sigaction(SIGALRM, &old, NULL); + alarm(30); + errno = e; + return r; +} + +static long departed_drive_REAPURBNDELAY(int f, unsigned long req) +{ + void *urbp = NULL; + return ioctl(f, req, &urbp); +} + +static long departed_drive_GET_CAPABILITIES(int f, unsigned long req) +{ + uint32_t caps = 0; + return ioctl(f, req, &caps); +} + +static long departed_drive_GET_SPEED(int f, unsigned long req) +{ + return ioctl(f, req, NULL); +} + +static long departed_drive_CONNECTINFO(int f, unsigned long req) +{ + static struct connectinfo ci; + return ioctl(f, req, &ci); +} + +static long departed_drive_DISCSIGNAL(int f, unsigned long req) +{ + struct disconnectsignal ds = {.signr = 0, .context = NULL}; + return ioctl(f, req, &ds); +} + +/* The three rows that reach usbdev_ioctl's default arm: usbdevfs requests the + * layer defines and dispatches nowhere. Their arguments are the ones Linux + * reads for them, so that nothing but the arm decides the answer. + */ +static long departed_drive_HUB_PORTINFO(int f, unsigned long req) +{ + static struct hub_portinfo hp; + return ioctl(f, req, &hp); +} + +static long departed_drive_ALLOC_STREAMS(int f, unsigned long req) +{ + struct streams st = {.num_streams = 2, .num_eps = 1}; + st.eps[0] = EP_IN; + return ioctl(f, req, &st); +} + +static long departed_drive_WAIT_FOR_RESUME(int f, unsigned long req) +{ + return ioctl(f, req, NULL); +} + + +/* setup */ + +/* The request code the generated table records for one usbdevfs ioctl, for the + * two calls the setup makes on its own account rather than as a row. + */ +static unsigned long request_of(const char *req_name) +{ + for (int i = 0; i < USBDEV_DEPARTED_NROWS; i++) + if (!strcmp(usbdev_departed_rows[i].req_name, req_name)) + return usbdev_departed_rows[i].request; + return 0; +} + +static long fx_terminate(int f, unsigned ms) +{ + struct ctrltransfer ct = {.bRequestType = 0x40, + .bRequest = FX_TERMINATE, + .wValue = (uint16_t) ms, + .timeout = 1000, + .data = NULL}; + return ioctl(f, USBDEVFS_CONTROL, &ct); +} + +/* Bring the event thread up, which is what delivers the fixture's terminate, + * and take the watch it arms away again by closing the fd that armed it. + * + * One SUBMITURB does both. It is a default-control-pipe URB whose data lands in + * an unmapped page, which is the shape that reaches the ep0 event source -- and + * so usbdev_loop_get and the watch -- and then fails on its own argument + * without a claim, without a wire call and without anything to reap. Any fd + * left holding a watch would be told the device left, and its peer walk would + * stamp every other fd on the node, which is exactly what the rows below need + * not to have happened yet. + */ +static bool start_event_thread(void) +{ + long pgsz = sysconf(_SC_PAGESIZE); + uint8_t *pg = mmap(NULL, (size_t) pgsz * 2, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (pg == MAP_FAILED || munmap(pg + pgsz, (size_t) pgsz) != 0) + return false; + uint8_t *setup = pg + pgsz - 8; + setup[0] = 0x40; /* vendor, host-to-device: no implicit claim */ + setup[1] = 0x01; + setup[2] = setup[3] = setup[4] = setup[5] = 0; + setup[6] = 16; /* wLength; the data begins at the unmapped page */ + setup[7] = 0; + + int f = open(NODE, O_RDWR); + if (f < 0) { + munmap(pg, (size_t) pgsz); + return false; + } + struct urb u; + memset(&u, 0, sizeof(u)); + u.type = URB_TYPE_CONTROL; + u.endpoint = 0; + u.buffer = setup; + u.buffer_length = 8 + 16; + long r = ioctl(f, request_of("USBDEVFS_SUBMITURB"), &u); + int e = errno; + close(f); + munmap(pg, (size_t) pgsz); + return r < 0 && e == EFAULT; +} + +/* reporting */ + +static const char *revents_name(int mask) +{ + if (mask == 0) + return "NONE"; + if (mask == (POLLERR | POLLHUP)) + return "ERRHUP"; + static char other[32]; + snprintf(other, sizeof(other), "0x%x", (unsigned) mask); + return other; +} + +static const char *errno_name(int e) +{ + switch (e) { + case 0: + return "NONE"; + case EAGAIN: + return "EAGAIN"; + case EBUSY: + return "EBUSY"; + case EINTR: + return "EINTR"; + case EINVAL: + return "EINVAL"; + case ENODATA: + return "ENODATA"; + case ENODEV: + return "ENODEV"; + case ENOENT: + return "ENOENT"; + case ENOTTY: + return "ENOTTY"; + case EPERM: + return "EPERM"; + case EPROTO: + return "EPROTO"; + default: { + static char other[16]; + snprintf(other, sizeof(other), "errno%d", e); + return other; + } + } +} + +static void print_tuple(char *out, + size_t n, + long rc, + int e, + int stamp, + int peer) +{ + snprintf(out, n, "%ld/%s/%s/%s", rc, errno_name(rc < 0 ? e : 0), + revents_name(stamp), revents_name(peer)); +} + +/* the run */ + +static bool run_row(const usbdev_departed_row_t *row, int subject, int peer) +{ + errno = 0; + long rc = usbdev_departed_drivers[row - usbdev_departed_rows](subject, + row->request); + int e = errno; + int stamp = revents_of(subject); + int pv = revents_of(peer); + + const usbdev_departed_tuple_t *want = + row->diverges ? &row->here : &row->kernel; + bool ok = rc == want->rc && (rc >= 0 || e == want->err) && + stamp == want->stamp && pv == want->peer; + + char got[96], expect[96]; + print_tuple(got, sizeof(got), rc, e, stamp, pv); + print_tuple(expect, sizeof(expect), want->rc, want->err, want->stamp, + want->peer); + + TEST(row->id); + if (ok) + PASS(); + else + FAILF("%s: got %s, want %s", row->id, got, expect); + + if (row->diverges) { + /* The gap must still be a gap: an XFAIL that has quietly started + * answering what Linux answers is a row to retire, not to keep. + */ + char kern[96]; + print_tuple(kern, sizeof(kern), row->kernel.rc, row->kernel.err, + row->kernel.stamp, row->kernel.peer); + TEST("and still differs from Linux"); + CHECK(!(rc == row->kernel.rc && (rc >= 0 || e == row->kernel.err) && + stamp == row->kernel.stamp && pv == row->kernel.peer), + "%s: now answers Linux's %s; retire the XFAIL row", row->id, + kern); + } + + printf(" %-24s %-8s %-22s %-22s devio.c:%d\n", row->id, + row->diverges ? "XFAIL" : "match", got, + row->diverges ? expect : "(Linux)", row->devio_line); + return ok; +} + +int main(int argc, char **argv) +{ + setvbuf(stdout, NULL, _IOLBF, 0); + signal(SIGALRM, on_alarm); + enter("startup"); + + if (argc != 2) { + printf("usage: %s fresh|held-claim|held-release\n", argv[0]); + return 2; + } + int phase; + if (!strcmp(argv[1], "fresh")) + phase = USBDEV_DEPARTED_FRESH; + else if (!strcmp(argv[1], "held-claim")) + phase = USBDEV_DEPARTED_HELD_CLAIM; + else if (!strcmp(argv[1], "held-release")) + phase = USBDEV_DEPARTED_HELD_RELEASE; + else { + printf("unknown phase '%s'\n", argv[1]); + return 2; + } + + printf("usbdevfs ioctl surface on a departed loopback device (%s)\n", + argv[1]); + + TEST("the event thread is up and holds no watch"); + CHECK(start_event_thread(), "could not arm the loopback event thread"); + + /* One fd per row, and the peer opened before the call so it is a peer of + * it. In the held phases the subject is opened first instead, because its + * claim has to be taken while the device is still there. + */ + int held = -1; + if (phase != USBDEV_DEPARTED_FRESH) { + held = open(NODE, O_RDWR); + unsigned int ifn = IFNUM; + TEST("an fd claims the interface while the device is here"); + CHECK(held >= 0 && + ioctl(held, request_of("USBDEVFS_CLAIMINTERFACE"), &ifn) == 0, + "claim rc=%d errno=%d", held, errno); + } + + int term = phase == USBDEV_DEPARTED_FRESH ? open(NODE, O_RDWR) : held; + + /* The other half of the DISCARDURB row, taken while the device is still + * here: an unknown URB is -EINVAL, and only the departure turns that into + * -ENODEV. A layer that answered -ENODEV to every discard would satisfy the + * row and fail this. + */ + struct urb unknown; + memset(&unknown, 0, sizeof(unknown)); + errno = 0; + long dr = term >= 0 + ? ioctl(term, request_of("USBDEVFS_DISCARDURB"), &unknown) + : 0; + TEST("an unknown URB is EINVAL while the device is here"); + CHECK(term >= 0 && dr < 0 && errno == EINVAL, "DISCARDURB rc=%ld errno=%d", + dr, errno); + + /* The other half of the three default-arm rows, taken while the device is + * still here. Those rows want -ENODEV from an arm that used to answer + * -ENOTTY, and an arm rewritten to answer -ENODEV always would satisfy + * every one of them. -ENOTTY is what the arm owes a device that is there. + */ + int unimpl_ok = term >= 0; + for (int i = 0; i < USBDEV_DEPARTED_NROWS && unimpl_ok; i++) { + const usbdev_departed_row_t *row = &usbdev_departed_rows[i]; + if (strcmp(row->id, "HUB_PORTINFO") && + strcmp(row->id, "ALLOC_STREAMS") && + strcmp(row->id, "WAIT_FOR_RESUME")) + continue; + errno = 0; + long r = usbdev_departed_drivers[i](term, row->request); + if (r != -1 || errno != ENOTTY) { + unimpl_ok = false; + FAILF("%s while the device is here: rc=%ld errno=%s", row->id, r, + errno_name(errno)); + } + } + TEST("an unimplemented request is ENOTTY while the device is here"); + CHECK(unimpl_ok, "an unimplemented request did not answer ENOTTY"); + + TEST("the device is terminated"); + CHECK(term >= 0 && fx_terminate(term, 30) >= 0, "terminate errno=%d", + errno); + if (phase == USBDEV_DEPARTED_FRESH) + close(term); + settle(300); + + if (held >= 0) { + /* The premise of these rows: a synchronous-only fd armed no watch, so + * it still holds a claim on a device it has not been told about. If + * that stopped being true the rows below would pass against the gate at + * the top of usbdev_ioctl and prove nothing. + */ + TEST("the claiming fd was told nothing"); + CHECK(revents_of(held) == 0, "revents=0x%x", revents_of(held)); + } + + printf(" %-24s %-8s %-22s %-22s %s\n", "row", "answer", "measured", + "Linux", "cite"); + int driven = 0, want = 0; + for (int i = 0; i < USBDEV_DEPARTED_NROWS; i++) { + const usbdev_departed_row_t *row = &usbdev_departed_rows[i]; + if (row->phase != phase) + continue; + want++; + enter(row->id); + int subject, peer; + if (held >= 0) { + subject = held; + peer = open(NODE, O_RDWR); + } else { + peer = open(NODE, O_RDWR); + subject = open(NODE, O_RDWR); + } + if (subject < 0 || peer < 0) { + TEST(row->id); + FAILF("%s: open errno=%d", row->id, errno); + continue; + } + (void) run_row(row, subject, peer); + driven++; + close(peer); + if (held < 0) + close(subject); + } + if (held >= 0) + close(held); + + /* The other half of the contract, and the half no row above can reach. + * Every row asks on an fd that has NOT been told the device left, which is + * what makes the per-request answers interesting; docs/internals.md's + * universal is about the fd that HAS been told, and unpinned it was a + * sentence rather than a measurement. On a marked fd the whole surface + * answers -ENODEV -- the gate at the top of usbdev_ioctl is ahead of every + * arm including the default one, and the two reaps let past it decide the + * same way for themselves once nothing is left to hand back. + */ + if (phase == USBDEV_DEPARTED_FRESH) { + enter("stamped fd"); + int st = open(NODE, O_RDWR); + errno = 0; + long crc = + st >= 0 ? departed_drive_CONTROL(st, request_of("USBDEVFS_CONTROL")) + : 0; + int cerr = errno, cre = st >= 0 ? revents_of(st) : 0; + TEST("an fd that reached the wire carries the mark"); + CHECK( + st >= 0 && crc < 0 && cerr == ENODEV && cre == (POLLERR | POLLHUP), + "CONTROL rc=%ld errno=%s revents=%s", crc, errno_name(cerr), + revents_name(cre)); + + char first[160]; + first[0] = '\0'; + int uniform = 0; + for (int i = 0; i < USBDEV_DEPARTED_NROWS && st >= 0; i++) { + const usbdev_departed_row_t *row = &usbdev_departed_rows[i]; + enter(row->id); + errno = 0; + long rc = usbdev_departed_drivers[i](st, row->request); + int e = errno, re = revents_of(st); + if (rc == -1 && e == ENODEV && re == (POLLERR | POLLHUP)) { + uniform++; + } else if (!first[0]) { + snprintf(first, sizeof(first), "%s answered %ld/%s/%s", row->id, + rc, errno_name(rc < 0 ? e : 0), revents_name(re)); + } + } + enter("stamped fd"); + TEST("and then answers ENODEV to the whole usbdevfs surface"); + CHECK(st >= 0 && uniform == USBDEV_DEPARTED_NROWS, + "%d of %d requests; %s", uniform, USBDEV_DEPARTED_NROWS, + first[0] ? first : "no fd"); + + /* Where that universal stops. do_vfs_ioctl answers these for every file + * before it calls f_op->unlocked_ioctl, so on Linux they never reach + * usbfs and never meet connected(); here they are answered ahead of the + * gate and ahead of the default arm's device question. What this + * asserts is that the answer does not move with what the fd has been + * told -- not that -ENOTTY is Linux's answer, which for eight of the + * ten it is not. Those eight, and what Linux gives instead, are + * recorded and printed by check_vfs_ioctls in tests/test-usbdev-ioctl.c + * rather than asserted here. Both fds are driven because the two used + * to answer -ENODEV for different reasons: the marked one from the + * gate, the fresh one from the ask. + */ + static const struct { + const char *name; + unsigned long request; + } vfs_first[] = { + {"FIOQSIZE", 0x5460ul}, + {"FIGETBSZ", 0x00000002ul}, + {"FIFREEZE", 0xc0045877ul}, + {"FITHAW", 0xc0045878ul}, + {"FS_IOC_FIEMAP", 0xc020660bul}, + {"FICLONE", 0x40049409ul}, + {"FICLONERANGE", 0x4020940dul}, + {"FIDEDUPERANGE", 0xc0189436ul}, + {"FS_IOC_GETFSUUID", 0x80111500ul}, + {"FS_IOC_GETFSSYSFSPATH", 0x80811501ul}, + }; + const int nvfs = (int) (sizeof(vfs_first) / sizeof(vfs_first[0])); + int vf = open(NODE, O_RDWR); + for (int which = 0; which < 2; which++) { + int subject = which == 0 ? st : vf; + int want_revents = which == 0 ? (POLLERR | POLLHUP) : 0; + char firstbad[160]; + firstbad[0] = '\0'; + int enotty = 0; + for (int i = 0; i < nvfs && subject >= 0; i++) { + enter(vfs_first[i].name); + unsigned char scratch[64] = {0}; + errno = 0; + long rc = ioctl(subject, vfs_first[i].request, scratch); + int e = errno, re = revents_of(subject); + if (rc == -1 && e == ENOTTY && re == want_revents) + enotty++; + else if (!firstbad[0]) + snprintf(firstbad, sizeof(firstbad), + "%s answered %ld/%s/%s", vfs_first[i].name, rc, + errno_name(rc < 0 ? e : 0), revents_name(re)); + } + enter(which == 0 ? "stamped fd" : "fresh fd"); + TEST(which == 0 + ? "the mark does not reach what do_vfs_ioctl answers first" + : "and neither does the default arm's device question"); + CHECK(subject >= 0 && enotty == nvfs, "%d of %d requests; %s", + enotty, nvfs, firstbad[0] ? firstbad : "no fd"); + } + + /* The other request docs/internals.md names for the marked fd. io.c + * answers FIONBIO for these fds, so the mark cannot reach it either. + */ + enter("stamped fd"); + int on = 1; + errno = 0; + long nbrc = st >= 0 ? ioctl(st, 0x5421ul, &on) : -1; + int nbe = errno, nbre = st >= 0 ? revents_of(st) : 0; + TEST("nor FIONBIO, which io.c answers ahead of this layer"); + CHECK(st >= 0 && nbrc == 0 && nbre == (POLLERR | POLLHUP), + "FIONBIO rc=%ld errno=%s revents=%s", nbrc, + errno_name(nbrc < 0 ? nbe : 0), revents_name(nbre)); + + if (vf >= 0) + close(vf); + if (st >= 0) + close(st); + } + + enter("summary"); + TEST("every row of this phase was driven"); + CHECK(driven == want && want > 0, "drove %d of %d rows", driven, want); + + printf("\n"); + for (int i = 0; i < USBDEV_DEPARTED_NROWS; i++) { + const usbdev_departed_row_t *row = &usbdev_departed_rows[i]; + if (row->phase == phase && row->diverges) + printf(" XFAIL %s: %s\n", row->id, row->note); + } + + SUMMARY("test-usbdev-ioctl-departed"); + return fails ? 1 : 0; +} diff --git a/tests/test-usbdev-ioctl.c b/tests/test-usbdev-ioctl.c index 24d8000a..ead9e724 100644 --- a/tests/test-usbdev-ioctl.c +++ b/tests/test-usbdev-ioctl.c @@ -99,6 +99,7 @@ static long pwritev2_raw(int fd, #define USBDEVFS_CONNECTINFO 0x40085511u #define USBDEVFS_IOCTL 0xc0105512u #define USBDEVFS_SUBMITURB 0x8038550au +#define USBDEVFS_REAPURBNDELAY 0x4008550du #define USBDEVFS_GET_CAPABILITIES 0x8004551au #define USBDEVFS_DISCONNECT_CLAIM 0x8108551bu #define USBDEVFS_GET_SPEED 0x0000551fu @@ -134,6 +135,26 @@ struct disconnect_claim { char driver[256]; }; +/* struct usbdevfs_urb; the ioctl encodes 0x38 = 56 bytes of it. */ +struct usburb { + unsigned char type, endpoint; + int status; + unsigned int flags; + void *buffer; + int buffer_length, actual_length, start_frame, number_of_packets; + int error_count; + unsigned int signr; + void *usercontext; +}; + +#define URB_TYPE_ISO 0 +#define URB_TYPE_INTERRUPT 1 +#define URB_TYPE_CONTROL 2 +#define URB_TYPE_BULK 3 + +/* USBFS_XFER_MAX (devio.c:140). */ +#define URB_XFER_MAX (0xffffffffu / 2u - 1000000u) + /* ioctl(2) collapses every failure onto -1; the assertions below are about * which errno, so report it as a negative value the way the kernel does. */ @@ -274,6 +295,9 @@ static void check_seek(void) /* claimintf refuses ifnum >= 8 * sizeof(unsigned long) -- 64, not 32. Between * the two, an interface number is merely absent, which is a question about the * device rather than about the argument. + * + * Which side 64 itself falls on depends on what the bound stands for upstream, + * and it is not the same for every op here: see the two blocks at the end. */ static void check_interface_bound(void) { @@ -320,16 +344,47 @@ static void check_interface_bound(void) EXPECT_EQ(io(fd, USBDEVFS_SETINTERFACE, &si), -EINVAL, "setinterface 64/256"); + /* The same number, the other side of the split above: 64 is an argument + * error to claimintf and a device answer to these two. Neither + * proc_disconnect_claim nor proc_ioctl bounds the number at all -- their + * -EINVAL is usb_ifnum_to_if coming back NULL (devio.c:2471-2473 and + * devio.c's proc_ioctl, which repeats connected() for itself) -- so the + * bound here stands in for that lookup, and a lookup cannot outrank the + * device it would have been performed on. This node has no IOKit object + * behind it, so it is that device, and it answers -ENODEV for every + * interface number. It used to answer -EINVAL for 64 and -ENODEV for 63, + * which named a missing interface on a device that was missing entirely. + * + * CLAIMINTERFACE, RELEASEINTERFACE and SETINTERFACE keep -EINVAL for 64 + * because claimintf really does bound it, against the width of + * ps->ifclaimed rather than against anything the device says. Linux runs + * connected() ahead of that bound too, so on a device that has gone it + * answers -ENODEV where this answers -EINVAL; the deviation is recorded + * rather than closed, because closing it would put an IOKit enumeration + * ahead of a check that reads no device state. + */ struct disconnect_claim dc; memset(&dc, 0, sizeof(dc)); dc.interface = 64; - TEST("DISCONNECT_CLAIM 64 is EINVAL"); - EXPECT_EQ(io(fd, USBDEVFS_DISCONNECT_CLAIM, &dc), -EINVAL, "dc 64"); + TEST("DISCONNECT_CLAIM 64 asks the device before it judges the number"); + EXPECT_EQ(io(fd, USBDEVFS_DISCONNECT_CLAIM, &dc), -ENODEV, "dc 64"); struct usbdevfs_ioctl ic = { .ifno = 64, .ioctl_code = (int) USBDEVFS_DISCONNECT, .data = NULL}; - TEST("USBDEVFS_IOCTL ifno 64 is EINVAL"); - EXPECT_EQ(io(fd, USBDEVFS_IOCTL, &ic), -EINVAL, "usbdevfs_ioctl 64"); + TEST("USBDEVFS_IOCTL ifno 64 asks the device before it judges the number"); + EXPECT_EQ(io(fd, USBDEVFS_IOCTL, &ic), -ENODEV, "usbdevfs_ioctl 64"); + ic.ifno = -1; + TEST("USBDEVFS_IOCTL ifno -1 answers the same"); + EXPECT_EQ(io(fd, USBDEVFS_IOCTL, &ic), -ENODEV, "usbdevfs_ioctl -1"); + + /* RESET makes no IOKit call of its own with nothing claimed -- its pipe + * loop has nothing to walk -- so it ran to the end and returned 0 on a node + * with no device behind it. libusb_reset_device is the recovery call an + * application reaches for after an error, so success there is the answer + * that keeps it from finding out. connected() runs before proc_resetdevice. + */ + TEST("RESET with nothing claimed still asks the device"); + EXPECT_EQ(io(fd, USBDEVFS_RESET, NULL), -ENODEV, "reset"); close(fd); } @@ -763,6 +818,86 @@ static void check_retire_window_race(void) printf(" slots: %d before, %d after\n", before, after); } +/* proc_do_submiturb's argument order, which is not do_proc_bulk's. + * + * The synchronous ioctl above resolves the endpoint before it looks at the + * length, and the assertions in check_endpoint_arguments pin that. SUBMITURB + * runs the same two checks the other way round: devio.c:1644-1661 rejects the + * flags mask and USBFS_XFER_MAX first, and only then calls findintfep, so a + * length past the bound outranks a missing endpoint and everything else does + * not. The async path was first written with the synchronous order and answered + * -EINVAL for four requests Linux rejects by endpoint, and had no XFER_MAX + * bound at all -- on the default control pipe, where no endpoint lookup runs, a + * 2 GB URB was accepted outright. + * + * All of it is decided before any transfer, so the fixture reaches every case. + */ +static void check_urb_arguments(void) +{ + printf("\ntest-usbdev-ioctl: SUBMITURB argument order\n"); + int fd = open(NODE, O_RDWR); + if (fd < 0) { + TEST("open for the URB arguments"); + FAIL("open"); + return; + } + char scratch[64]; + struct usburb u = {.type = URB_TYPE_BULK, + .endpoint = 0x05, /* absent on the modeled device */ + .buffer = scratch, + .buffer_length = 8}; + + TEST("an undefined URB flag outranks the endpoint"); + u.flags = 0x100u; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb flag 0x100"); + TEST("ISO_ASAP on a bulk URB outranks the endpoint"); + u.flags = 0x02u; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb ISO_ASAP"); + u.flags = 0; + + TEST("a length past USBFS_XFER_MAX outranks the endpoint"); + u.buffer_length = 0x7fffffff; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb INT_MAX"); + TEST("exactly USBFS_XFER_MAX is EINVAL"); + u.buffer_length = (int) URB_XFER_MAX; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb XFER_MAX"); + TEST("a negative length is EINVAL"); + u.buffer_length = -1; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb -1"); + + TEST("a null buffer with a positive length outranks the endpoint"); + u.buffer = NULL; + u.buffer_length = 64; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb null buffer"); + u.buffer = scratch; + + TEST("an absent endpoint outranks an unknown transfer type"); + u.type = 99; + u.buffer_length = 8; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -ENOENT, "urb type 99 ep 0x05"); + TEST("an absent endpoint outranks the ISO rejection"); + u.type = URB_TYPE_ISO; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -ENOENT, "urb iso ep 0x05"); + TEST("an absent endpoint outranks the control setup-length check"); + u.type = URB_TYPE_CONTROL; + u.buffer_length = 4; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -ENOENT, "urb control ep 0x05"); + TEST("a reserved-bit endpoint is EINVAL"); + u.endpoint = 0x30; + u.buffer_length = 8; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb ep 0x30"); + + /* The default control pipe skips the endpoint lookup (devio.c:1651), so the + * length bound is the only thing between this request and a 2 GB + * allocation. + */ + TEST("the default control pipe still honors USBFS_XFER_MAX"); + u.endpoint = 0; + u.buffer_length = 0x7fffffff; + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, &u), -EINVAL, "urb ep0 INT_MAX"); + close(fd); +} + static void check_answers_without_a_device(void) { printf("\ntest-usbdev-ioctl: what is answered from the model\n"); @@ -773,12 +908,13 @@ static void check_answers_without_a_device(void) return; } - /* Every capability bit names part of the URB machinery this stage answers - * ENOTTY for, so the word is 0 until that machinery lands. + /* The capability word names exactly what the URB engine honors: ZERO_PACKET + * and REAP_AFTER_DISCONNECT. BULK_CONTINUATION is accepted without its + * error-cascade unlink, so its bit stays clear. */ uint32_t caps = 0xffffffffu; - TEST("GET_CAPABILITIES reports no URB capabilities"); - EXPECT_TRUE(io(fd, USBDEVFS_GET_CAPABILITIES, &caps) == 0 && caps == 0, + TEST("GET_CAPABILITIES names what the URB engine honors"); + EXPECT_TRUE(io(fd, USBDEVFS_GET_CAPABILITIES, &caps) == 0 && caps == 0x11u, "caps"); TEST("GET_SPEED returns the enum as its value"); @@ -793,13 +929,29 @@ static void check_answers_without_a_device(void) ci.slow == 0, "connectinfo"); - TEST("an unknown ioctl is ENOTTY"); - EXPECT_EQ(io(fd, 0x00005563u /* _IO('U', 99) */, NULL), -ENOTTY, + /* The device question outranks the unknown request, as connected() outranks + * usbdev_do_ioctl's switch (devio.c:2638) and the -ENOTTY is that switch's + * default. This node has no IOKit object behind it, so -ENODEV is the whole + * answer; the -ENOTTY the arm gives once the enumeration has run is pinned + * on a live device by tests/test-usbdev-ioctl-departed.c, and what the arm + * answers on a departed one is a row in tests/usbdev-ioctl-departed.tbl. + */ + TEST("an unknown ioctl asks the device before it refuses"); + EXPECT_EQ(io(fd, 0x00005563u /* _IO('U', 99) */, NULL), -ENODEV, "unknown ioctl"); - TEST("SUBMITURB is ENOTTY at this stage"); - EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, NULL), -ENOTTY, "submiturb"); - TEST("DISCARDURB is ENOTTY at this stage"); - EXPECT_EQ(io(fd, USBDEVFS_DISCARDURB, NULL), -ENOTTY, "discardurb"); + TEST("SUBMITURB refuses an unreadable URB"); + EXPECT_EQ(io(fd, USBDEVFS_SUBMITURB, NULL), -EFAULT, "submiturb"); + + /* -EINVAL is proc_unlinkurb's answer for a URB this fd is not holding, and + * it is the answer on a device that is there; this node has none behind it, + * so the discard's own scan is not what decides. The scan reads the pending + * list, which is this layer's bookkeeping and says nothing about the + * device, so it used to answer -EINVAL here and reach libusb as + * LIBUSB_ERROR_NOT_FOUND on an unplug. The -EINVAL half is asserted where a + * device exists, in tests/test-usbdev-ioctl-departed.c's setup. + */ + TEST("DISCARDURB of an unknown URB asks the device first"); + EXPECT_EQ(io(fd, USBDEVFS_DISCARDURB, NULL), -ENODEV, "discardurb"); /* Everything that has to reach the wire says so, with the errno Linux uses * for a device that is not there. @@ -827,22 +979,81 @@ static void check_answers_without_a_device(void) /* FIONBIO and FIOASYNC never reach a file's own ioctl handler on Linux: * do_vfs_ioctl answers both for every file before it calls f_op->unlocked_ioctl - * (fs/ioctl.c:818-822), so they also never meet usbdevfs's FMODE_WRITE gate. - * Sent into it here they came back EPERM on a read-only fd and ENOTTY on a - * writable one, while fcntl(F_SETFL) on the same descriptor set O_NONBLOCK and - * F_GETFL reported it: two entry points onto one flag, disagreeing about it. + * (fs/ioctl.c:507-511 at v6.18), so they also never meet usbdevfs's FMODE_WRITE + * gate. Sent into it here they came back EPERM on a read-only fd and ENOTTY on + * a writable one, while fcntl(F_SETFL) on the same descriptor set O_NONBLOCK + * and F_GETFL reported it: two entry points onto one flag, disagreeing about + * it. * * Measured on Linux (gcc:14, a char device and a plain file, access modes 0, 1, * 2 and 3): FIONBIO(1) is 0 and sets O_NONBLOCK in every one of them; * FIOASYNC(1) is ENOTTY and FIOASYNC(0) is 0, because ioctl_fioasync only * consults f_op->fasync when the request would change the FASYNC state and * usbdev_file_operations declares none (devio.c:2846-2856). + * + * The ten beside them are the rest of that set, and one sentence decides all + * twelve: the access mode is not theirs to meet, so each answers alike on a + * read-only and on a writable fd. usbdev_vfs_answers_first sits ABOVE this + * layer's FMODE_WRITE gate for exactly that, and the read-only half of the loop + * below is what says so -- moved under that gate, every one of the ten turns + * EPERM there while the writable half stays green. + * + * What the ten answer here is not what Linux answers, so the table carries both + * values the way every other recorded gap in this lane does. This layer models + * none of them and gives -ENOTTY to all ten; Linux agrees on two and the other + * eight are deliberate divergences, printed as XFAIL at the end of this + * function. Measured on Linux 7.0.14 (uname -r + * 7.0.14-orbstack-00380-ga7e0a2dc9535, aarch64, gcc:14, unprivileged, a + * 256-byte zeroed argument buffer -- the shape this lane sends, wide enough for + * the largest _IOC_SIZE among the ten, the 129 bytes FS_IOC_GETFSSYSFSPATH + * declares, rather than for the answer they all give) against a real + * /dev/bus/usb/BBB/DDD node on a devtmpfs mount. That is the kernel that ran; + * the line numbers cited below are v6.18, which is the source they were read + * from, and the two are named apart because a cite and a measurement are + * different claims. The superblock those arms consult is the one the node sits + * on, which is devtmpfs and not usbfs: devtmpfs is shmem-backed + * (devtmpfs.c:69), so it has a block size (shmem.c:5071) and a generated UUID + * (shmem.c:5082-5084), and two arms answer on that rather than -ENOTTY. */ +static const struct { + const char *name; + unsigned long request; + const char *linux_answer; /* measured, not derived from the name */ + const char *cite; /* the fs/ioctl.c arm it stops in */ + const char *note; /* NULL where Linux gives -ENOTTY too */ +} vfs_first[] = { + {"FIOQSIZE", 0x5460ul, "-1/ENOTTY", "ioctl.c:513-522", NULL}, + {"FIGETBSZ", 0x00000002ul, "0, writing s_blocksize 4096", "ioctl.c:533-538", + "the arm is reached and answers from the superblock"}, + {"FIFREEZE", 0xc0045877ul, "-1/EPERM", "ioctl.c:385-394", + "ioctl_fsfreeze stops at CAP_SYS_ADMIN, and -1/EOPNOTSUPP past it"}, + {"FITHAW", 0xc0045878ul, "-1/EPERM", "ioctl.c:402-412", + "ioctl_fsthaw stops at the same capability, and -1/EINVAL past it on a " + "superblock nobody froze"}, + {"FS_IOC_FIEMAP", 0xc020660bul, "-1/EOPNOTSUPP", "ioctl.c:206-207", + "the inode carries no fiemap operation, which is its own answer"}, + {"FICLONE", 0x40049409ul, "-1/EBADF", "ioctl.c:237-238", + "the argument is an fd, not a pointer, so a pointer names no open file"}, + {"FICLONERANGE", 0x4020940dul, "-1/EINVAL", "ioctl.c:250-258", + "the zeroed argument names fd 0 as the source, and -1/EXDEV instead when " + "fd 0 sits on another superblock"}, + {"FIDEDUPERANGE", 0xc0189436ul, "-1/EINVAL", "ioctl.c:415-452", + "vfs_dedupe_file_range refuses a source that is not a regular file " + "(remap_range.c:515-516), which is its own answer: the same zeroed " + "argument on a regular file is 0, and on a directory it is -1/EISDIR"}, + {"FS_IOC_GETFSUUID", 0x80111500ul, "0", "ioctl.c:455-465", + "the shmem-backed devtmpfs every distro mounts carries a generated UUID, " + "so the arm answers; a ramfs-backed one has none and refuses"}, + {"FS_IOC_GETFSSYSFSPATH", 0x80811501ul, "-1/ENOTTY", "ioctl.c:468-473", + NULL}, +}; + static void check_vfs_ioctls(void) { - printf("\ntest-usbdev-ioctl: the two ioctls the vfs answers first\n"); + printf("\ntest-usbdev-ioctl: the ioctls the vfs answers first\n"); const int modes[2] = {O_RDONLY, O_RDWR}; const char *names[2] = {"read-only", "writable"}; + const int nvfs = (int) (sizeof(vfs_first) / sizeof(vfs_first[0])); for (int i = 0; i < 2; i++) { int fd = open(NODE, modes[i]); if (fd < 0) { @@ -885,6 +1096,26 @@ static void check_vfs_ioctls(void) snprintf(t, sizeof(t), "FIOASYNC(0) on a %s fd is 0", names[i]); TEST(t); EXPECT_EQ(io(fd, FIOASYNC, &zero), 0, "fioasync 0"); + + /* And the ten, under the same claim. The read-only pass is the one that + * pins the carve-out above the FMODE_WRITE gate rather than below it; + * -ENOTTY here is this layer's answer, not Linux's, and the XFAIL lines + * below carry what Linux gives. + */ + for (int v = 0; v < nvfs; v++) { + /* Wide enough for the largest _IOC_SIZE among the ten: + * FS_IOC_GETFSSYSFSPATH declares 129 bytes. Nothing writes it while + * every one of them stops at -ENOTTY, and sizing it to the request + * rather than to that answer is what keeps it true if one stops + * doing so. + */ + unsigned char scratch[256] = {0}; + snprintf(t, sizeof(t), "%s on a %s fd is ENOTTY", vfs_first[v].name, + names[i]); + TEST(t); + EXPECT_EQ(io(fd, vfs_first[v].request, scratch), -ENOTTY, + vfs_first[v].name); + } close(fd); } @@ -899,6 +1130,16 @@ static void check_vfs_ioctls(void) EXPECT_EQ(io(fd, FIOASYNC, (void *) 8), -EFAULT, "fioasync efault"); close(fd); } + + for (int v = 0; v < nvfs; v++) { + if (!vfs_first[v].note) + continue; + printf( + " XFAIL vfs-first-%s: Linux answers %s (%s), elfuse -1/ENOTTY; " + "%s\n", + vfs_first[v].name, vfs_first[v].linux_answer, vfs_first[v].cite, + vfs_first[v].note); + } } /* Every gate on this descriptor derives its capability bits the way OPEN_FMODE @@ -1121,6 +1362,79 @@ static void check_close_identity(void) CHURN_ROUNDS, churn_bad); } +/* A non-blocking reap whose fd number is closed and reopened between the + * fd-table window the pass takes and the side-table lookup that has to answer + * for it. + * + * Those were two windows: an fd_snapshot in the reap, and another one inside + * usbdev_acquire proving the generation against its own read. A close and + * reopen in between satisfied the second and not the first, so the pass ran on + * the new description's side-table entry while still holding the old one's + * readiness pipe -- it settled the readiness level on, and a blocking reap + * would have parked on, whatever host fd had taken that number. One window + * answers for one open file description, and the reopened fd is not that + * description, so the pass belongs to nothing: EBADF, the answer this side + * table already gives a generation mismatch. + * + * The window is a few instructions wide unaided, so this runs under + * ELFUSE_USBDEV_REAP_DELAY_US. The reopen names the other bus for the reason + * the publish race does: two entries describing one device are + * indistinguishable by anything the guest can read. + */ +static void check_reap_window_race(void) +{ + printf("\ntest-usbdev-ioctl: a close and reopen inside the reap window\n"); + int before = count_table_slots(); + int fd = open(NODE, O_RDWR); + if (fd < 0) { + TEST("an fd to reap on"); + FAIL("open"); + return; + } + + /* Nothing has been submitted, so an undisturbed pass is EAGAIN: the device + * is reachable and has nothing to hand back. That is the answer the race + * has to change, and the answer the two-window form kept giving -- from the + * wrong description. + */ + void *out = NULL; + TEST("an undisturbed non-blocking reap is EAGAIN"); + EXPECT_EQ(io(fd, USBDEVFS_REAPURBNDELAY, &out), -EAGAIN, "quiet reap"); + + race_fd = fd; + race_sibling = -1; + race_reopen_node = OTHER_NODE; + pthread_t t; + if (pthread_create(&t, NULL, race_closer, NULL) != 0) { + TEST("closer thread for the reap race"); + FAIL("pthread_create"); + close(fd); + return; + } + long r = io(fd, USBDEVFS_REAPURBNDELAY, &out); + pthread_join(t, NULL); + race_reopen_node = NULL; + int sib = race_sibling; + + TEST("the reopen took the number the close freed"); + EXPECT_TRUE(sib == fd, "same fd number"); + TEST("the reap answers for no description, not for the new one"); + EXPECT_EQ(r, -EBADF, "reap across the swap"); + + /* The description that owns the number now is untouched by any of it. */ + TEST("the reopened fd answers for the device it was opened on"); + EXPECT_EQ(fd_vid(sib), OTHER_VID, "idVendor"); + TEST("and its own reap is the quiet EAGAIN"); + EXPECT_EQ(io(sib, USBDEVFS_REAPURBNDELAY, &out), -EAGAIN, "sibling reap"); + + if (sib >= 0) + close(sib); + int after = count_table_slots(); + TEST("neither entry leaks its slot"); + EXPECT_EQ(after, before, "slots after the race"); + printf(" slots: %d before, %d after\n", before, after); +} + /* Deviations this stage keeps deliberately: printed with both values so the gap * is in the lane's output rather than only in the commit message. */ @@ -1131,18 +1445,48 @@ static void print_known_gaps(void) if (fd >= 0) { long r = io(fd, USBDEVFS_RESET, NULL); printf( - " XFAIL reset: Linux re-enumerates the port, elfuse clears " - "claimed pipes' stalls and returns %ld\n", + " XFAIL reset: Linux re-enumerates the port, elfuse asks the " + "device first and then kills its URBs and clears claimed pipes' " + "stalls, logging rather than reporting a clear that fails; on this " + "node, which has no device behind it, that first question answers " + "%ld\n", r); - long speed = io(fd, USBDEVFS_GET_SPEED, NULL); - printf( - " XFAIL disconnect-gate: Linux answers ENODEV for every ioctl " - "once the device is gone, elfuse still serves GET_SPEED, " - "CONNECTINFO, GET_CAPABILITIES and read() from the open-time " - "model (GET_SPEED here: %ld)\n", - speed); close(fd); } + printf( + " XFAIL clear-halt-collateral: Linux warns and leaves a queued URB on " + "the endpoint alone (check_reset_of_active_ep, devio.c:1382-1394), " + "elfuse has only ClearPipeStallBothEnds, which aborts the pipe, so " + "CLEAR_HALT and RESETEP make an in-flight URB reap -ECONNRESET\n"); + printf( + " XFAIL clear-halt-shutter: Linux has no per-endpoint abort shutter " + "to keep, elfuse keeps one (ep_aborting) on DISCARDURB and on every " + "wholesale kill and does not raise it for CLEAR_HALT or RESETEP, so a " + "queued follower there can be started behind a stall clear's abort " + "that is still in flight\n"); + printf( + " XFAIL printer-device-id: Linux lets a printer's GET_DEVICE_ID " + "through untouched, reading wIndex as interface<<8|altsetting when " + "usb_find_alt_setting(actconfig, wIndex >> 8, wIndex & 0xff) is " + "USB_CLASS_PRINTER (check_ctrlrecip, devio.c), elfuse reads " + "wIndex & 0xff as the interface number for every non-vendor interface " + "recipient and implicitly claims that one\n"); + printf( + " XFAIL urb-signal: Linux raises the URB's signr at completion " + "(kill_pid_usb_asyncio, devio.c:657) and DISCSIGNAL's at disconnect, " + "elfuse accepts both, returns 0 and delivers neither\n"); + printf( + " XFAIL iso: Linux serves isochronous URBs, elfuse answers EINVAL " + "once the endpoint has resolved\n"); + printf( + " XFAIL discard-latency: DISCARDURB is proc_unlinkurb, which calls " + "usb_kill_urb -- the synchronous one, guaranteeing the URB is idle on " + "return -- and NOT usb_unlink_urb, the asynchronous unlink that " + "returns -EINPROGRESS; so elfuse waits for IOKit's abort callback to " + "match it, and the divergence is only the ceiling: Linux never gives " + "up, elfuse gives up after 2s rather than parking the vCPU thread " + "against a wire that may never answer, leaving the record flagged and " + "still reapable when its completion arrives\n"); printf( " XFAIL driver-name: Linux GETDRIVER reports the driver's name " "(cdc_acm), elfuse reports the IOKit class (AppleUSBACMControl), and " @@ -1194,6 +1538,11 @@ int main(void) SUMMARY("test-usbdev-ioctl"); return fails > 0 ? 1 : 0; } + if (getenv("ELFUSE_USBDEV_REAP_DELAY_US")) { + check_reap_window_race(); + SUMMARY("test-usbdev-ioctl"); + return fails > 0 ? 1 : 0; + } if (!strcmp(mode, "badifnum")) { check_malformed_interface_number(); SUMMARY("test-usbdev-ioctl"); @@ -1206,6 +1555,7 @@ int main(void) check_interface_bound(); check_endpoint_arguments(); check_offset_and_vector_edges(); + check_urb_arguments(); check_answers_without_a_device(); check_vfs_ioctls(); check_access_mode_three(); diff --git a/tests/test-usbdev-urb-host.c b/tests/test-usbdev-urb-host.c new file mode 100644 index 00000000..e5c9627e --- /dev/null +++ b/tests/test-usbdev-urb-host.c @@ -0,0 +1,169 @@ +/* + * Native-host unit tests for the URB bookkeeping that needs no device + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The async engine's own lane (tests/test-usbdev-ioctl.c) runs against + * ELFUSE_USB_FIXTURE, where there is no IOKit service to complete a transfer, + * so everything past SUBMITURB's argument gate is unreachable from it. That is + * how a 2190-line engine came to ship with two constant compares behind it, and + * how five of the defects a first review found -- a refcon that lost half the + * slot table, a transferred count copied straight through from the device, a + * URB count cap Linux does not have, an argument order the synchronous path + * next door already had right, and an endpoint queue that handed AbortPipe a + * bystander -- all sat in code no test executed. + * + * What is testable without a device is the arithmetic, so src/syscall/ + * usbdev-urb.h holds it and this binary exercises it directly on any machine, + * board attached or not. Each case below fails on the shape that shipped: the + * refcon cases fail with a four-bit index field, the argument cases fail + * without USBFS_XFER_MAX, the clamp cases fail when the device's count is + * copied through, and the queue cases fail when an endpoint with an abort in + * flight is allowed to start its successor. + */ + +#include +#include +#include +#include + +#include "syscall/usbdev-urb.h" + +static int passes, fails; + +static void check(bool ok, const char *what) +{ + if (ok) { + passes++; + } else { + fails++; + printf(" FAIL %s\n", what); + } +} + +/* Every slot the table has must survive a round trip, at generations whose low + * bits collide with the index field. A four-bit index decoded slot 16+k as slot + * k with the generation one higher, which both lost that slot's disconnect and + * could deliver it to a live fd sitting in slot k. + */ +static void check_watch_refcon(void) +{ + printf("test-usbdev-urb-host: disconnect-watch refcon\n"); + for (unsigned slot = 0; slot < USBDEV_MAX_FDS; slot++) { + for (uint64_t gen = 0; gen < 8; gen++) { + uintptr_t tok = usbdev_watch_pack(slot, gen); + unsigned got_slot = ~0u; + uint64_t got_gen = ~UINT64_C(0); + bool ok = usbdev_watch_unpack(tok, &got_slot, &got_gen); + check(ok && got_slot == slot && got_gen == gen, + "slot/generation round trip"); + } + } + + /* No two live (slot, generation) pairs may share a token: that is the + * aliasing that marked an attached device gone. + */ + for (unsigned a = 0; a < USBDEV_MAX_FDS; a++) { + for (unsigned b = a + 1; b < USBDEV_MAX_FDS; b++) + check(usbdev_watch_pack(a, 7) != usbdev_watch_pack(b, 7), + "two slots never share one token"); + } + + /* The generation field must still be wide enough that a slot cannot be + * reused often enough to alias itself. + */ + check(USBDEV_WATCH_GEN_MASK >= (UINT64_C(1) << 50) - 1, + "generation field stays wide"); +} + +/* proc_do_submiturb's first gate, in the kernel's order. */ +static void check_arg_gate(void) +{ + printf("test-usbdev-urb-host: SUBMITURB argument gate\n"); + check(usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0, 64, false) == 0, + "a plain bulk URB is accepted"); + check(usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0x100, 64, false) == + -LINUX_EINVAL, + "an undefined flag bit is EINVAL"); + check(usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, LINUX_URB_ISO_ASAP, 64, + false) == -LINUX_EINVAL, + "ISO_ASAP on a bulk URB is EINVAL"); + check(usbdev_urb_arg_check(LINUX_URB_TYPE_ISO, LINUX_URB_ISO_ASAP, 64, + false) == 0, + "ISO_ASAP on an ISO URB passes the mask"); + check(usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0, -1, false) == + -LINUX_EINVAL, + "a negative length is EINVAL through the unsigned compare"); + + /* USBFS_XFER_MAX, the bound that was missing outright: without it these + * lengths reached the memory budget and answered -ENOMEM, and on the + * default control pipe the largest of them was accepted. + */ + check(usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0, 0x7fffffff, false) == + -LINUX_EINVAL, + "INT32_MAX is EINVAL, not ENOMEM"); + check( + usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0, (int32_t) USBDEV_XFER_MAX, + false) == -LINUX_EINVAL, + "exactly USBFS_XFER_MAX is EINVAL"); + check(usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0, + (int32_t) USBDEV_XFER_MAX - 1, false) == 0, + "one below USBFS_XFER_MAX passes the gate"); + check( + usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0, 64, true) == -LINUX_EINVAL, + "a null buffer with a positive length is EINVAL"); + check(usbdev_urb_arg_check(LINUX_URB_TYPE_BULK, 0, 0, true) == 0, + "a null buffer with no length is fine"); +} + +/* urb->actual_length never exceeds the buffer the guest handed over. */ +static void check_actual_clamp(void) +{ + printf("test-usbdev-urb-host: transferred-count clamp\n"); + check(usbdev_urb_clamp_actual(0, 64) == 0, "zero stays zero"); + check(usbdev_urb_clamp_actual(18, 64) == 18, "a short count passes"); + check(usbdev_urb_clamp_actual(64, 64) == 64, "an exact count passes"); + check(usbdev_urb_clamp_actual(65, 64) == 64, "one byte over is clamped"); + check(usbdev_urb_clamp_actual(UINT64_C(0xffffffff), 64) == 64, + "a wild count is clamped, not reported"); + check(usbdev_urb_clamp_actual(4096, 0) == 0, + "a zero-length URB reports nothing transferred"); +} + +static void check_zlp_predicate(void) +{ + printf("test-usbdev-urb-host: ZERO_PACKET predicate\n"); + check(usbdev_urb_needs_zlp(0, true, 1, 64, 64), "a maxpacket multiple"); + check(!usbdev_urb_needs_zlp(0, false, 1, 64, 64), "flag clear"); + check(!usbdev_urb_needs_zlp(-32, true, 1, 64, 64), "failed URB"); + check(!usbdev_urb_needs_zlp(0, true, 0, 64, 64), "ep0 is excluded"); + check(!usbdev_urb_needs_zlp(0, true, 1, 0, 64), "no data"); + check(!usbdev_urb_needs_zlp(0, true, 1, 100, 64), "not a multiple"); + check(!usbdev_urb_needs_zlp(0, true, 1, 64, 0), "unknown maxpacket"); +} + +/* AbortPipe's granularity is the whole pipe, so the FIFO stays shut until the + * abort that is running has returned. + */ +static void check_ep_gate(void) +{ + printf("test-usbdev-urb-host: endpoint start gate\n"); + check(usbdev_ep_may_start(false, 0, false), "idle endpoint starts"); + check(!usbdev_ep_may_start(false, 0, true), "busy endpoint waits"); + check(!usbdev_ep_may_start(false, 1, false), + "an abort in flight holds the successor back"); + check(!usbdev_ep_may_start(true, 0, false), "a slot being drained is shut"); + check(!usbdev_ep_may_start(true, 2, true), "all three at once"); +} + +int main(void) +{ + check_watch_refcon(); + check_arg_gate(); + check_actual_clamp(); + check_zlp_predicate(); + check_ep_gate(); + printf("test-usbdev-urb-host: %d passed, %d failed\n", passes, fails); + return fails ? 1 : 0; +} diff --git a/tests/test-usbdev-urb-loopback.c b/tests/test-usbdev-urb-loopback.c new file mode 100644 index 00000000..4d75361b --- /dev/null +++ b/tests/test-usbdev-urb-loopback.c @@ -0,0 +1,2671 @@ +/* + * The async URB engine against a loopback device (ELFUSE_USB_FIXTURE=loopback) + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Code under test: the async half of src/syscall/usbdev.c and the usbdevfs arms + * of src/syscall/poll.c. The other usbdevfs lane runs against fixture devices + * with no IOKit service behind them, so it stops at SUBMITURB's argument gate; + * this one runs against a device whose IOKit answers come from + * src/syscall/usbdev-fixture.c, so a URB can actually complete. Everything + * between the ioctl and the wire is the production path: the bounce buffers, + * the memory budget, the per-endpoint FIFO, the completion callback on the + * event thread, the readiness and disconnect maps, REAPURB and the drain. + * + * The fixture is told what to do by a script (see usbdev-fixture.c) that this + * binary rewrites through a vendor control request before each scenario, and it + * keeps a log of what actually crossed the seam, which is how the trailing + * zero-length packet is observed rather than inferred. + * + * The device is /dev/bus/usb/003/001, interface 2, bulk OUT 0x02, bulk IN 0x81, + * interrupt IN 0x83, matching the out-of-tree board driver's endpoints. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +int passes = 0, fails = 0; + +/* The guest headers may or may not spell it; poll.c must answer for it either + * way, which is the whole of what the epoll half below asserts. + */ +#ifndef EPOLLWRNORM +#define EPOLLWRNORM 0x100 +#endif + +#define NODE "/dev/bus/usb/003/001" + +/* A usbdevfs node that is not the fixture's, for the one scenario that is about + * a guest fd number outliving the device it was opened on. + */ +#define OTHER_NODE "/dev/bus/usb/001/001" +#define IFNUM 2 +#define EP_OUT 0x02 +#define EP_IN 0x81 +#define EP_INT 0x83 +#define MPS 64 + +#define USBDEVFS_CONTROL 0xc0185500u +#define USBDEVFS_SUBMITURB 0x8038550au +#define USBDEVFS_DISCARDURB 0x0000550bu +#define USBDEVFS_REAPURB 0x4008550cu +#define USBDEVFS_REAPURBNDELAY 0x4008550du +#define USBDEVFS_SETINTERFACE 0x80085504u +#define USBDEVFS_SETCONFIGURATION 0x80045505u +#define USBDEVFS_CLAIMINTERFACE 0x8004550fu +#define USBDEVFS_RELEASEINTERFACE 0x80045510u +#define USBDEVFS_GET_CAPABILITIES 0x8004551au +#define USBDEVFS_CLEAR_HALT 0x80045515u +#define USBDEVFS_RESETEP 0x80045503u +#define USBDEVFS_RESET 0x00005514u +#define USBDEVFS_GETDRIVER 0x41045508u +#define USBDEVFS_IOCTL 0xc0105512u +#define USBDEVFS_DISCONNECT_CLAIM 0x8108551bu +#define USBDEVFS_IOCTL_DISCONNECT 0x5516u +#define USBDEVFS_IOCTL_CONNECT 0x5517u + +struct getdriver { + unsigned int interface; + char driver[256]; +}; + +struct usbdevfs_ioctl { + int ifno, ioctl_code; + void *data; +}; + +struct disconnect_claim { + unsigned int interface, flags; + char driver[256]; +}; + +#define URB_TYPE_INTERRUPT 1 +#define URB_TYPE_CONTROL 2 +#define URB_TYPE_BULK 3 +#define URB_SHORT_NOT_OK 0x01u +#define URB_ZERO_PACKET 0x40u + +struct ctrltransfer { + uint8_t bRequestType, bRequest; + uint16_t wValue, wIndex, wLength; + uint32_t timeout; + void *data; +}; + +struct setinterface { + unsigned int interface, altsetting; +}; + +struct urb { + unsigned char type, endpoint; + int status; + unsigned int flags; + void *buffer; + int buffer_length, actual_length, start_frame; + union { + int number_of_packets; + unsigned int stream_id; + } u; + int error_count; + unsigned int signr; + void *usercontext; +}; + +static int fd = -1; + +/* A broken engine does not fail a blocking REAPURB, it never answers it, and a + * lane that hangs is worse in CI than one that fails. Every scenario re-arms + * this, and the handler names the scenario that ran out of time. + */ +static const char *stage = "startup"; + +static void on_alarm(int sig) +{ + (void) sig; + static char pre[] = "\nTIMEOUT in stage: "; + (void) !write(1, pre, sizeof(pre) - 1); + (void) !write(1, stage, strlen(stage)); + (void) !write(1, "\n", 1); + _exit(1); +} + +static void enter(const char *name) +{ + stage = name; + alarm(30); +} + +static char msgbuf[256]; +#define FAILF(...) \ + do { \ + snprintf(msgbuf, sizeof(msgbuf), __VA_ARGS__); \ + FAIL(msgbuf); \ + } while (0) +#define CHECK(cond, ...) \ + do { \ + if (cond) \ + PASS(); \ + else \ + FAILF(__VA_ARGS__); \ + } while (0) + +static long io(unsigned long req, void *arg) +{ + int r = ioctl(fd, req, arg); + return r < 0 ? -errno : r; +} + +static double now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; +} + +/* the fixture's control plane */ + +#define FX_LOG 0xf0 +#define FX_SCRIPT 0xf1 +#define FX_TERMINATE 0xf2 +#define FX_RESET 0xf3 +#define FX_STATS 0xf4 +#define FX_REC 32 + +static long fx_script(const char *s) +{ + struct ctrltransfer ct = {.bRequestType = 0x40, + .bRequest = FX_SCRIPT, + .wValue = 0, + .wIndex = 0, + .wLength = (uint16_t) strlen(s), + .timeout = 1000, + .data = (void *) (uintptr_t) s}; + return io(USBDEVFS_CONTROL, &ct); +} + +static long fx_reset(void) +{ + struct ctrltransfer ct = {.bRequestType = 0x40, + .bRequest = FX_RESET, + .timeout = 1000, + .data = NULL}; + return io(USBDEVFS_CONTROL, &ct); +} + +static long fx_terminate(unsigned ms) +{ + struct ctrltransfer ct = {.bRequestType = 0x40, + .bRequest = FX_TERMINATE, + .wValue = (uint16_t) ms, + .timeout = 1000, + .data = NULL}; + return io(USBDEVFS_CONTROL, &ct); +} + +/* Make every AbortPipe and USBDeviceAbortPipeZero answer kIOReturnNoDevice and + * cancel nothing, without terminating the device: the one state an abort can + * report that no other op reports for it. + */ +static long fx_aborts_gone(void) +{ + struct ctrltransfer ct = {.bRequestType = 0x40, + .bRequest = FX_TERMINATE, + .wValue = 0, + .wIndex = 4, + .timeout = 1000, + .data = NULL}; + return io(USBDEVFS_CONTROL, &ct); +} + +/* Device handles the layer released while the fixture still owned a transfer on + * the default control pipe. Readable after the fd that leaked one is gone, + * which is the point: the release happens at close. + */ +static uint32_t fx_dev_releases_in_flight(void) +{ + uint8_t buf[4] = {0}; + struct ctrltransfer ct = {.bRequestType = 0xc0, + .bRequest = FX_STATS, + .wValue = 0, + .wIndex = 0, + .wLength = sizeof(buf), + .timeout = 1000, + .data = buf}; + if (io(USBDEVFS_CONTROL, &ct) != (long) sizeof(buf)) + return 0xffffffffu; + return (uint32_t) buf[0] | ((uint32_t) buf[1] << 8) | + ((uint32_t) buf[2] << 16) | ((uint32_t) buf[3] << 24); +} + +/* Tear the claimed interface's pipes down without touching anything else, so + * GetPipeProperties alone answers NoDevice. + */ +static long fx_pipes_gone(void) +{ + struct ctrltransfer ct = {.bRequestType = 0x40, + .bRequest = FX_TERMINATE, + .wValue = 0, + .wIndex = 2, + .timeout = 1000, + .data = NULL}; + return io(USBDEVFS_CONTROL, &ct); +} + +static uint8_t logbuf[4096]; +static int nlog; + +static int fx_readlog(void) +{ + struct ctrltransfer ct = {.bRequestType = 0xc0, + .bRequest = FX_LOG, + .wValue = 0, + .wIndex = 0, + .wLength = sizeof(logbuf), + .timeout = 1000, + .data = logbuf}; + long r = io(USBDEVFS_CONTROL, &ct); + nlog = r > 0 ? (int) (r / FX_REC) : 0; + return nlog; +} + +static unsigned rec_u32(int i, int off) +{ + const uint8_t *p = logbuf + (size_t) i * FX_REC + off; + return (unsigned) p[0] | ((unsigned) p[1] << 8) | ((unsigned) p[2] << 16) | + ((unsigned) p[3] << 24); +} +static unsigned rec_kind(int i) +{ + return logbuf[(size_t) i * FX_REC]; +} +static unsigned rec_ep(int i) +{ + return logbuf[(size_t) i * FX_REC + 1]; +} +static unsigned rec_conc(int i) +{ + return logbuf[(size_t) i * FX_REC + 3]; +} +static unsigned rec_req(int i) +{ + return rec_u32(i, 4); +} +static unsigned rec_actual(int i) +{ + return rec_u32(i, 8); +} +static unsigned rec_start(int i) +{ + return rec_u32(i, 20); +} +static unsigned rec_data(int i, int byte) +{ + return logbuf[(size_t) i * FX_REC + 24 + byte]; +} + +/* Wait out something that has to happen on the event thread before the next + * scenario starts -- a late abort completing an orphaned record, say. + */ +static void settle(int ms) +{ + struct timespec ts = {ms / 1000, (long) (ms % 1000) * 1000 * 1000}; + nanosleep(&ts, NULL); +} + +/* urb helpers */ + +static void mk_bulk(struct urb *u, unsigned char ep, void *buf, int len) +{ + memset(u, 0, sizeof(*u)); + u->type = URB_TYPE_BULK; + u->endpoint = ep; + u->buffer = buf; + u->buffer_length = len; +} + +static long submit(struct urb *u) +{ + return io(USBDEVFS_SUBMITURB, u); +} + +static struct urb *reap(bool block) +{ + struct urb *out = NULL; + long r = io(block ? USBDEVFS_REAPURB : USBDEVFS_REAPURBNDELAY, &out); + return r == 0 ? out : NULL; +} + +/* A blocking REAPURB is the right thing to assert against an engine that still + * queues, and the wrong thing to assert against one that has lost the URB: it + * turns a failed assertion into a hung lane. Where a scenario's own break can + * swallow a URB, poll for it instead and let the absence be the failure. + */ +static struct urb *reap_within(int ms) +{ + for (int waited = 0; waited <= ms; waited += 5) { + struct urb *d = reap(false); + if (d) + return d; + struct timespec ts = {0, 5 * 1000 * 1000}; + nanosleep(&ts, NULL); + } + return NULL; +} + +/* Hand back everything outstanding so one scenario cannot leak into the next. + */ +static void quiesce(void) +{ + for (int i = 0; i < 32; i++) { + struct urb *d = reap(false); + if (!d) + break; + } +} + +static int poll_fd_once(int f, short events, int timeout_ms) +{ + struct pollfd p = {.fd = f, .events = events, .revents = 0}; + int r = poll(&p, 1, timeout_ms); + return r <= 0 ? 0 : p.revents; +} + +static int poll_once(short events, int timeout_ms) +{ + return poll_fd_once(fd, events, timeout_ms); +} + +/* Close the fd every scenario runs on and open a fresh one. + * + * A disconnect is a fact about the device, not about the fd that noticed it, so + * a scenario that provokes one on any fd leaves every other fd on the node + * stamped too -- the main one included. The fixture's device is still there + * (nothing was terminated), so a new open gets an undisconnected fd back. + */ +static void reopen_main(int *prev) +{ + unsigned ifn = IFNUM; + close(*prev); + fd = open(NODE, O_RDWR); + *prev = fd; + if (fd >= 0) + io(USBDEVFS_CLAIMINTERFACE, &ifn); +} + +/* scenarios */ + +static void t_complete_with_data(void) +{ + enter("t_complete_with_data"); + uint8_t out[16], in[64]; + struct urb uo, ui; + for (int i = 0; i < 16; i++) + out[i] = (uint8_t) (0x50 + i); + memset(in, 0, sizeof(in)); + fx_reset(); + fx_script("ep02:ok;ep81:ok"); + + mk_bulk(&uo, EP_OUT, out, sizeof(out)); + TEST("OUT submit accepted"); + CHECK(submit(&uo) == 0, "SUBMITURB OUT rc=%d", errno); + struct urb *d = reap(true); + TEST("OUT reaps itself"); + CHECK(d == &uo, "reaped %p want %p", (void *) d, (void *) &uo); + TEST("OUT status 0 actual 16"); + CHECK(uo.status == 0 && uo.actual_length == 16, "status=%d actual=%d", + uo.status, uo.actual_length); + + mk_bulk(&ui, EP_IN, in, sizeof(in)); + TEST("IN submit accepted"); + CHECK(submit(&ui) == 0, "SUBMITURB IN rc=%d", errno); + d = reap(true); + TEST("IN reaps the bytes the OUT wrote"); + CHECK(d == &ui && ui.status == 0 && ui.actual_length == 16 && + memcmp(in, out, 16) == 0, + "status=%d actual=%d first=%02x", ui.status, ui.actual_length, in[0]); + TEST("IN wrote nothing past actual_length"); + CHECK(in[16] == 0 && in[63] == 0, "in[16]=%02x in[63]=%02x", in[16], + in[63]); + quiesce(); +} + +static void t_short(void) +{ + enter("t_short"); + uint8_t in[64]; + struct urb u; + fx_reset(); + fx_script("ep81:short(8)*2"); + memset(in, 0xee, sizeof(in)); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + reap(true); + TEST("short IN is success with the short count"); + CHECK(u.status == 0 && u.actual_length == 8, "status=%d actual=%d", + u.status, u.actual_length); + TEST("short IN left the tail of the buffer alone"); + CHECK(in[8] == 0xee && in[63] == 0xee, "in[8]=%02x in[63]=%02x", in[8], + in[63]); + + memset(in, 0xee, sizeof(in)); + mk_bulk(&u, EP_IN, in, sizeof(in)); + u.flags = URB_SHORT_NOT_OK; + submit(&u); + reap(true); + TEST("SHORT_NOT_OK turns a short IN into -EREMOTEIO"); + CHECK(u.status == -EREMOTEIO && u.actual_length == 8, "status=%d actual=%d", + u.status, u.actual_length); + + /* A device-supplied count larger than the buffer: every Linux HCD bounds + * urb->actual_length by transfer_buffer_length, so a guest that copies + * actual_length bytes out stays inside its own allocation. + */ + fx_reset(); + fx_script("ep81:ok(999)"); + mk_bulk(&u, EP_IN, in, 32); + submit(&u); + reap(true); + TEST("an over-reported transferred count is clamped to the buffer"); + CHECK(u.status == 0 && u.actual_length == 32, "status=%d actual=%d", + u.status, u.actual_length); + quiesce(); +} + +static void t_error_status(void) +{ + enter("t_error_status"); + uint8_t in[32]; + struct urb u; + struct { + const char *script; + int want; + const char *name; + } rows[] = { + {"ep81:stall", -EPIPE, "kIOUSBPipeStalled reaps -EPIPE"}, + {"ep81:timeout", -ETIMEDOUT, + "kIOUSBTransactionTimeout reaps -ETIMEDOUT"}, + {"ep81:err", -EPROTO, "an unmapped IOReturn reaps -EPROTO"}, + }; + for (unsigned i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + fx_reset(); + fx_script(rows[i].script); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + reap(true); + TEST(rows[i].name); + CHECK(u.status == rows[i].want, "status=%d want=%d", u.status, + rows[i].want); + } + + /* refuse: IOKit turns the start itself down, and the URB has to come back + * through the URB-status map rather than the syscall map, whose Aborted row + * would write -EINTR into urb->status. + */ + fx_reset(); + fx_script("ep81:refuse(0xe00002cd)"); + mk_bulk(&u, EP_IN, in, sizeof(in)); + long rc = submit(&u); + TEST("a start IOKit refuses is a SUBMITURB error"); + CHECK(rc == -ENODEV, "submit rc=%ld", rc); + quiesce(); + + /* The refusal above cannot tell the two maps apart: kIOReturnNotOpen is + * -ENODEV in both. kIOReturnAborted is the row where they differ, and only + * a queued follower can reach it, because the leader's refusal is a syscall + * return value and the follower's is a urb->status. The syscall map answers + * -EINTR there, which is a value the kernel never writes into a URB. + */ + uint8_t lead[16], follow[16]; + struct urb ul, uf; + fx_reset(); + fx_script("ep02:delay(60),ok,refuse(0xe00002eb)"); + memset(lead, 0x11, sizeof(lead)); + memset(follow, 0x22, sizeof(follow)); + mk_bulk(&ul, EP_OUT, lead, sizeof(lead)); + mk_bulk(&uf, EP_OUT, follow, sizeof(follow)); + submit(&ul); + submit(&uf); + struct urb *r1 = reap_within(2000); + struct urb *r2 = reap_within(2000); + TEST("a refused follower comes back through the URB-status map"); + CHECK(r1 == &ul && r2 == &uf && ul.status == 0 && uf.status == -ECONNRESET, + "lead=%d follow=%d r1=%p r2=%p", ul.status, uf.status, (void *) r1, + (void *) r2); + quiesce(); +} + +static void t_queue_order(void) +{ + enter("t_queue_order"); + uint8_t a[16], b[16]; + struct urb ua, ub; + fx_reset(); + fx_script("ep02:delay(80),ok,ok"); + memset(a, 0xa1, sizeof(a)); + memset(b, 0xb2, sizeof(b)); + mk_bulk(&ua, EP_OUT, a, sizeof(a)); + mk_bulk(&ub, EP_OUT, b, sizeof(b)); + submit(&ua); + submit(&ub); + struct urb *d1 = reap(true); + struct urb *d2 = reap(true); + TEST("both queued URBs come back, oldest first"); + CHECK(d1 == &ua && d2 == &ub, "d1=%p d2=%p", (void *) d1, (void *) d2); + + fx_readlog(); + int first = -1, second = -1; + for (int i = 0; i < nlog; i++) { + if (rec_kind(i) != 1 || rec_ep(i) != EP_OUT) + continue; + if (first < 0) + first = i; + else if (second < 0) + second = i; + } + TEST("the wire saw exactly two transfers on the endpoint"); + CHECK(first >= 0 && second >= 0, "first=%d second=%d nlog=%d", first, + second, nlog); + TEST("never more than one in flight per endpoint"); + CHECK(first >= 0 && second >= 0 && rec_conc(first) == 1 && + rec_conc(second) == 1, + "conc=%u,%u", first >= 0 ? rec_conc(first) : 0, + second >= 0 ? rec_conc(second) : 0); + TEST("the follower started only after the leader completed"); + CHECK(second >= 0 && rec_start(second) >= 70, "follower started at %u ms", + second >= 0 ? rec_start(second) : 0); + quiesce(); +} + +static void t_discard_inflight(void) +{ + enter("t_discard_inflight"); + uint8_t in[32], intr[8]; + struct urb ui, ux; + fx_reset(); + fx_script("ep81:never;ep83:delay(30),ok"); + mk_bulk(&ui, EP_IN, in, sizeof(in)); + memset(&ux, 0, sizeof(ux)); + ux.type = URB_TYPE_INTERRUPT; + ux.endpoint = EP_INT; + ux.buffer = intr; + ux.buffer_length = sizeof(intr); + submit(&ui); + submit(&ux); + TEST("DISCARDURB on an in-flight URB returns 0"); + CHECK(io(USBDEVFS_DISCARDURB, &ui) == 0, "discard rc=%d", errno); + struct urb *d = reap(false); + TEST("the discarded URB is reapable immediately, as -ENOENT"); + CHECK(d == &ui && ui.status == -ENOENT, "d=%p status=%d", (void *) d, + ui.status); + d = reap(true); + TEST("the sibling on another endpoint survives and completes"); + CHECK(d == &ux && ux.status == 0 && ux.actual_length == 8, + "d=%p status=%d actual=%d", (void *) d, ux.status, ux.actual_length); + quiesce(); +} + +static void t_discard_queued(void) +{ + enter("t_discard_queued"); + uint8_t a[16], b[16]; + struct urb ua, ub; + fx_reset(); + fx_script("ep81:never*2"); + mk_bulk(&ua, EP_IN, a, sizeof(a)); + mk_bulk(&ub, EP_IN, b, sizeof(b)); + submit(&ua); + submit(&ub); + TEST("DISCARDURB on the queued follower returns 0"); + CHECK(io(USBDEVFS_DISCARDURB, &ub) == 0, "discard rc=%d", errno); + struct urb *d = reap(false); + TEST("the queued follower reaps -ENOENT and the leader stays in flight"); + CHECK(d == &ub && ub.status == -ENOENT && reap(false) == NULL, + "d=%p status=%d", (void *) d, ub.status); + io(USBDEVFS_DISCARDURB, &ua); + d = reap(false); + TEST("the leader then reaps -ENOENT too"); + CHECK(d == &ua && ua.status == -ENOENT, "d=%p status=%d", (void *) d, + ua.status); + quiesce(); +} + +static void t_poll(void) +{ + enter("t_poll"); + uint8_t in[32]; + struct urb u; + fx_reset(); + fx_script("ep81:delay(150),ok"); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + + /* Asking for both bits, because do_pollfd masks the answer by the events + * the caller demanded: a poll for POLLOUT alone is answered POLLOUT alone, + * on Linux as here. + */ + short want = POLLOUT | POLLWRNORM; + int rev = poll_once(want, 40); + TEST("poll reports nothing while the URB is in flight"); + CHECK(rev == 0, "revents=0x%x", rev); + rev = poll_once(want, 2000); + TEST("poll reports POLLOUT|POLLWRNORM when the completion lands"); + CHECK(rev == (POLLOUT | POLLWRNORM), "revents=0x%x", rev); + reap(false); + rev = poll_once(want, 40); + TEST("poll goes quiet again once the completion is reaped"); + CHECK(rev == 0, "revents=0x%x", rev); + + /* Same question through epoll, which reaches the fd by a different path in + * poll.c (EVFILT_READ plus a per-registration EPOLLOUT flag). + */ + int ep = epoll_create1(0); + struct epoll_event ev = {.events = EPOLLOUT, .data.fd = fd}; + epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev); + fx_reset(); + fx_script("ep81:delay(150),ok"); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + struct epoll_event got; + int n = epoll_wait(ep, &got, 1, 40); + TEST("epoll reports nothing while the URB is in flight"); + CHECK(n == 0, "epoll_wait n=%d events=0x%x", n, n > 0 ? got.events : 0u); + n = epoll_wait(ep, &got, 1, 2000); + TEST("epoll reports EPOLLOUT when the completion lands"); + CHECK(n == 1 && (got.events & EPOLLOUT), "n=%d events=0x%x", n, + n > 0 ? got.events : 0u); + reap(false); + + /* usbdev_poll answers EPOLLOUT|EPOLLWRNORM and eventpoll has no mask of its + * own: ep_item_poll masks the file's revents by epi->event.events, so a + * registration naming only EPOLLWRNORM is asking for exactly one of the two + * bits the file raises and has to be woken by it. poll() and select() next + * door already answered the pair; epoll tested EPOLLOUT alone, so the same + * fd with the same completion pending was silent -- measured, poll with + * POLLWRNORM alone rc=1 revents=0x100 against epoll_wait with EPOLLWRNORM + * alone rc=0 after its full 500 ms timeout. + */ + struct pollfd pw = {.fd = fd, .events = POLLWRNORM}; + fx_reset(); + fx_script("ep81:delay(150),ok"); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + int pr = poll(&pw, 1, 2000); + TEST("poll woken by POLLWRNORM alone reports POLLWRNORM"); + CHECK(pr == 1 && pw.revents == POLLWRNORM, "rc=%d revents=0x%x", pr, + pr > 0 ? pw.revents : 0); + + ev.events = EPOLLWRNORM; + epoll_ctl(ep, EPOLL_CTL_MOD, fd, &ev); + double t0 = now_ms(); + n = epoll_wait(ep, &got, 1, 2000); + double dt = now_ms() - t0; + TEST("and epoll registered for EPOLLWRNORM alone is woken by it too"); + CHECK(n == 1 && got.events == (uint32_t) EPOLLWRNORM, + "n=%d events=0x%x after %.0f ms", n, n > 0 ? got.events : 0u, dt); + + /* Both bits when both were asked for, which is what the mask being the + * registration's rather than a constant is for. + */ + ev.events = EPOLLOUT | EPOLLWRNORM; + epoll_ctl(ep, EPOLL_CTL_MOD, fd, &ev); + n = epoll_wait(ep, &got, 1, 2000); + TEST("a registration naming both is given both"); + CHECK(n == 1 && got.events == (uint32_t) (EPOLLOUT | EPOLLWRNORM), + "n=%d events=0x%x", n, n > 0 ? got.events : 0u); + close(ep); + reap(false); + quiesce(); +} + +static void t_reap_modes(void) +{ + enter("t_reap_modes"); + uint8_t in[32]; + struct urb u; + fx_reset(); + struct urb *out = NULL; + TEST("REAPURBNDELAY with nothing pending is -EAGAIN"); + CHECK(io(USBDEVFS_REAPURBNDELAY, &out) == -EAGAIN, "rc=%d", errno); + + fx_script("ep81:delay(150),ok"); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + double t0 = now_ms(); + struct urb *d = reap(true); + double dt = now_ms() - t0; + TEST("a blocking REAPURB waits for the completion and returns it"); + CHECK(d == &u && dt >= 100.0, "d=%p waited %.0f ms", (void *) d, dt); + quiesce(); +} + +static void t_zero_packet(void) +{ + enter("t_zero_packet"); + uint8_t out[MPS]; + struct urb u; + memset(out, 0x5a, sizeof(out)); + + fx_reset(); + fx_script("ep02:ok"); + mk_bulk(&u, EP_OUT, out, MPS); + u.flags = URB_ZERO_PACKET; + submit(&u); + reap(true); + TEST("a maxpacket-multiple ZERO_PACKET OUT still succeeds"); + CHECK(u.status == 0 && u.actual_length == MPS, "status=%d actual=%d", + u.status, u.actual_length); + fx_readlog(); + int data = -1, zlp = -1; + for (int i = 0; i < nlog; i++) { + if (rec_ep(i) != EP_OUT) + continue; + if (rec_kind(i) == 1 && rec_req(i) == MPS && data < 0) + data = i; + else if (rec_kind(i) == 3 && data >= 0 && zlp < 0) + zlp = i; + } + TEST("the trailing zero-length packet reached the wire"); + CHECK(zlp > data && data >= 0 && rec_req(zlp) == 0 && rec_actual(zlp) == 0, + "data=%d zlp=%d nlog=%d", data, zlp, nlog); + + /* Not a maxpacket multiple: no terminating packet, which is the other half + * of the predicate. + */ + fx_reset(); + fx_script("ep02:ok"); + mk_bulk(&u, EP_OUT, out, MPS - 1); + u.flags = URB_ZERO_PACKET; + submit(&u); + reap(true); + fx_readlog(); + int any_zlp = 0; + for (int i = 0; i < nlog; i++) + if (rec_kind(i) == 3) + any_zlp = 1; + TEST("a short OUT gets no terminating packet"); + CHECK(!any_zlp, "found a zero-length write among %d records", nlog); + + /* A terminating packet the device rejects is the URB's failure, because on + * Linux that packet is part of the URB. + */ + fx_reset(); + fx_script("ep02:zlpfail"); + mk_bulk(&u, EP_OUT, out, MPS); + u.flags = URB_ZERO_PACKET; + submit(&u); + reap(true); + TEST("a failed terminating packet lands in urb->status"); + CHECK(u.status == -EPIPE, "status=%d", u.status); + quiesce(); +} + +/* The fixture's own contract, driven through its control plane rather than + * through the engine: a scenario that cannot trust what the fixture reports + * about itself cannot assert anything about the engine either. + */ +static void t_fixture_contract(void) +{ + enter("t_fixture_contract"); + uint8_t in[4]; + struct urb u; + + /* A script the control request cannot carry whole. The prefix parses, so + * installing it silently swapped this scenario's rules for different ones + * and answered that the whole request was transferred. + */ + char big[600]; + memset(big, 0, sizeof(big)); + memcpy(big, "ep81:ok", 7); + for (size_t k = 7; k + 9 < sizeof(big) - 1; k += 9) + memcpy(big + k, ",delay(0)", 9); + size_t used = strlen(big); + memcpy(big + used, ",ok", 3); + + fx_reset(); + fx_script("ep81:stall"); + long r = fx_script(big); + TEST( + "a script too long for the fixture's buffer is refused, not truncated"); + CHECK(r == -EINVAL, "rc=%ld len=%zu", r, strlen(big)); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + reap(true); + TEST("the refused script left the loaded one in place"); + CHECK(u.status == -EPIPE, "status=%d (the prefix would have said 0)", + u.status); + quiesce(); + + /* An IN script may report more bytes than the URB asked for. The log keeps + * the first eight bytes of the buffer, and read them to the reported count. + */ + fx_reset(); + fx_script("ep81:ok(16)"); + memset(in, 0, sizeof(in)); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + reap(true); + fx_readlog(); + int rec = -1; + for (int i = 0; i < nlog; i++) + if (rec_kind(i) == 1 && rec_ep(i) == EP_IN) + rec = i; + TEST("the wire log kept the over-reported IN transfer"); + CHECK(rec >= 0 && rec_req(rec) == 4 && rec_actual(rec) == 16, + "rec=%d requested=%u actual=%u", rec, rec >= 0 ? rec_req(rec) : 0, + rec >= 0 ? rec_actual(rec) : 0); + int past = 0; + for (int b = 4; b < 8; b++) + if (rec >= 0 && rec_data(rec, b) != 0) + past++; + TEST("its payload peek stopped at the four bytes the URB submitted"); + CHECK(past == 0, "%d of 4 bytes past the buffer were recorded", past); + quiesce(); +} + +/* A drain that misses its 2 s deadline is one answer, and the two ioctls that + * retire the handles the survivors hold have to give it the same weight. + */ +static void t_drain_deadline(void) +{ + enter("t_drain_deadline"); + uint8_t in[32]; + struct urb u; + unsigned ifn = IFNUM; + + /* SETINTERFACE renumbers this interface's pipeRefs. */ + fx_reset(); + fx_script("ep81:wedge"); + mk_bulk(&u, EP_IN, in, sizeof(in)); + submit(&u); + struct setinterface si = {IFNUM, 0}; + double t0 = now_ms(); + long r = io(USBDEVFS_SETINTERFACE, &si); + double dt = now_ms() - t0; + TEST("SETINTERFACE is refused when the URB drain misses its deadline"); + CHECK(r == -EBUSY && dt >= 1900.0, "rc=%ld after %.0f ms", r, dt); + settle(800); /* the late abort frees the orphaned record */ + quiesce(); + + /* SETCONFIGURATION tears the whole pipe table down, so it is the same + * question. Linux refuses it outright while any interface is claimed, so + * the transfer that will not drain has to be ep0's -- which is also the + * queue no interface claim covers, and the reason the kill is issued here + * at all. + */ + io(USBDEVFS_RELEASEINTERFACE, &ifn); + fx_reset(); + fx_script("ep0:wedge"); + uint8_t setup[8] = {0x40, 0x01, 0, 0, 0, 0, 0, 0}; + memset(&u, 0, sizeof(u)); + u.type = URB_TYPE_CONTROL; + u.endpoint = 0; + u.buffer = setup; + u.buffer_length = sizeof(setup); + long sub = submit(&u); + TEST("a control URB on ep0 is accepted with no interface claimed"); + CHECK(sub == 0, "SUBMITURB rc=%ld", sub); + unsigned cfg = 1; + t0 = now_ms(); + r = io(USBDEVFS_SETCONFIGURATION, &cfg); + dt = now_ms() - t0; + TEST("SETCONFIGURATION refuses on that same answer instead of ignoring it"); + CHECK(r == -EBUSY && dt >= 1900.0, "rc=%ld after %.0f ms", r, dt); + settle(800); + quiesce(); + io(USBDEVFS_CLAIMINTERFACE, &ifn); +} + +/* A queued follower that starts after the device has gone. It reaches the same + * IOKit answer SUBMITURB does, by a path that only the completion callback can + * take, and it used to be the one site that did not stamp the fd. + */ +static void t_follower_device_gone(void) +{ + enter("t_follower_device_gone"); + uint8_t a[16], b[16]; + struct urb ua, ub; + int prev = fd; + unsigned ifn = IFNUM; + + /* Its own fd, because the stamp is permanent -- but not private to it: the + * assertions at the end are that the main fd, which has submitted nothing + * on this scenario's behalf, is told as well. + */ + int fd2 = open(NODE, O_RDWR); + if (fd2 < 0) { + TEST("a second fd on the loopback node"); + FAILF("open rc=%d", errno); + return; + } + fd = fd2; + io(USBDEVFS_CLAIMINTERFACE, &ifn); + fx_reset(); + fx_script("ep81:never,refuse(0xe00002c0)"); + mk_bulk(&ua, EP_IN, a, sizeof(a)); + mk_bulk(&ub, EP_IN, b, sizeof(b)); + submit(&ua); + submit(&ub); /* queued behind the leader: no IOKit call yet */ + io(USBDEVFS_DISCARDURB, &ua); + + struct urb *d = reap_within(2000); + TEST("the discarded leader reaps -ENOENT"); + CHECK(d == &ua && ua.status == -ENOENT, "d=%p status=%d", (void *) d, + ua.status); + d = reap_within(2000); + TEST("the follower IOKit refuses with NoDevice reaps -ENODEV"); + CHECK(d == &ub && ub.status == -ENODEV, "d=%p status=%d", (void *) d, + ub.status); + + int rev = poll_once(POLLIN, 200); + TEST("that refusal marks the fd disconnected for pollers"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + rev); + uint32_t caps = 0; + long ic = io(USBDEVFS_GET_CAPABILITIES, &caps); + TEST("and a later ioctl no longer passes the disconnected gate"); + CHECK(ic == -ENODEV, "GET_CAPABILITIES rc=%ld", ic); + + /* The other fd on the same node. It submitted no URB for this scenario, so + * nothing it did could have learned the device was gone: the stamp has to + * reach it because usbdev_remove marks every open file on the device, not + * because it went looking. Measured before that walk existed: this fd's + * poll stayed 0x0000 and its GET_CAPABILITIES stayed 0 while fd2 above + * already had 0x0018 and -ENODEV. + */ + int prev_rev = poll_fd_once(prev, POLLIN, 500); + TEST("the disconnect reaches the other fd open on the same device"); + CHECK((prev_rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), + "peer revents=0x%x", prev_rev); + uint32_t pcaps = 0; + int pic = ioctl(prev, USBDEVFS_GET_CAPABILITIES, &pcaps); + TEST("and its ioctls answer ENODEV without having touched the device"); + CHECK(pic < 0 && errno == ENODEV, "peer GET_CAPABILITIES rc=%d errno=%d", + pic, errno); + + close(fd2); + fd = prev; + reopen_main(&prev); + TEST("a fresh fd on the still-present device is not disconnected"); + CHECK(fd >= 0 && io(USBDEVFS_GET_CAPABILITIES, &caps) == 0, "reopen rc=%d", + errno); +} + +/* The readiness pipe carries a level, not one byte per completion. + * + * There is no per-fd URB count cap, only the process-wide byte budget, and a + * zero-length URB costs just its record against that -- so a backlog of + * completions can be far larger than the 64 KiB the pipe holds. One byte per + * completion loses the surplus: those records stay reapable with nothing + * readable behind them, and once the guest has spent the bytes that did land, + * poll, select and epoll all answer 0 for an fd whose very next reap returns + * immediately. + * + * The backlog has to exist before the first reap -- a guest that reaps as the + * completions arrive never lets one build -- so it is built without the event + * thread at all: a leader that never completes, a queue of followers behind it, + * and one DISCARDURB. The abort completes the leader, and the FIFO restart runs + * straight down the queue completing every follower IOKit refuses, so all of + * them are on the completed list by the time the ioctl returns. + */ +static void t_ready_level(void) +{ + enter("t_ready_level"); + alarm(180); /* the flood is the slowest thing in this lane */ + quiesce(); + fx_reset(); + fx_script("ep02:never,refuse(0xe0005000)"); + + /* FLOOD is above any capacity a macOS pipe has (64 KiB) and inside the 16 + * MiB budget: the charge is the guest data plus the record, 160 bytes on + * this target, so the 8-byte leader books 168 and each zero-length follower + * 160, for 10560168 bytes over 66001 records. + */ + enum { FLOOD = 66000, TOKENS = 65536 }; + struct urb *fl = calloc(FLOOD, sizeof *fl); + if (!fl) { + TEST("room for the flood's URB structs"); + FAILF("calloc of %d urbs failed", FLOOD); + return; + } + + uint8_t lead_buf[8] = {0}; + struct urb lead; + mk_bulk(&lead, EP_OUT, lead_buf, sizeof lead_buf); + long lr = submit(&lead); + int n = 0; + for (; n < FLOOD; n++) { + fl[n].type = URB_TYPE_BULK; + fl[n].endpoint = EP_OUT; + fl[n].buffer = NULL; + fl[n].buffer_length = 0; + if (submit(&fl[n]) != 0) + break; + } + TEST("the byte budget takes more zero-length URBs than the pipe has bytes"); + CHECK(lr == 0 && n == FLOOD, "leader rc=%ld, %d of %d queued", lr, n, + FLOOD); + + /* One ioctl turns the whole queue into completions, with no window in which + * a reap could keep pace with them. + */ + long dr = io(USBDEVFS_DISCARDURB, &lead); + TEST("discarding the leader completes the whole queue behind it"); + CHECK(dr == 0, "DISCARDURB rc=%ld", dr); + + /* Spend every byte the pipe could have held. */ + int reaped = 0; + while (reaped < TOKENS && reap(false)) + reaped++; + TEST("the pipe's worth of reaps hand URBs back"); + CHECK(reaped == TOKENS, "reaped %d of %d", reaped, TOKENS); + + double t0 = now_ms(); + int rev = poll_once(POLLOUT | POLLWRNORM, 1000); + double poll_dt = now_ms() - t0; + + int epfd = epoll_create1(0); + struct epoll_event ev = {.events = EPOLLOUT, .data.fd = fd}, got; + epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); + t0 = now_ms(); + int en = epoll_wait(epfd, &got, 1, 1000); + double ep_dt = now_ms() - t0; + close(epfd); + + fd_set ws; + FD_ZERO(&ws); + FD_SET(fd, &ws); + struct timeval tv = {1, 0}; + t0 = now_ms(); + int sn = select(fd + 1, NULL, &ws, NULL, &tv); + double sel_dt = now_ms() - t0; + + /* Asked last, so all three waits above were answered while this completion + * was sitting there for them. + */ + struct urb *d = reap(false); + TEST("a completion the pipe had no byte for is reapable all along"); + CHECK(d != NULL, "REAPURBNDELAY found nothing"); + TEST("poll reports it once the pipe's bytes are spent"); + CHECK(rev == (POLLOUT | POLLWRNORM), "revents=0x%x after %.0f ms", rev, + poll_dt); + TEST("epoll reports it too"); + CHECK(en == 1 && (got.events & EPOLLOUT), "n=%d events=0x%x after %.0f ms", + en, en > 0 ? got.events : 0u, ep_dt); + TEST("and select"); + CHECK(sn == 1 && FD_ISSET(fd, &ws), "ret=%d after %.0f ms", sn, sel_dt); + + int rest = d ? 1 : 0; + while (reap(false)) + rest++; + int quiet = poll_once(POLLOUT | POLLWRNORM, 0); + TEST("the whole surplus reaps back and the fd goes quiet again"); + CHECK(rest == n + 1 - reaped && quiet == 0, "%d of %d left, revents=0x%x", + rest, n + 1 - reaped, quiet); + free(fl); +} + +/* An orphaned record's charge follows its buffer, not the deadline. + * + * The drain's timeout arm settles the slot's own counters early so a reused + * slot never sees a late callback move them. The process-wide budget is not one + * of those: it accounts live memory, and the record's buffer is still allocated + * and still owned by an in-flight IOKit transfer when the deadline passes. Hand + * it back there and the accounting under-counts until the late callback runs, + * and for good if it never does. + */ +static bool budget_takes_9mb(uint8_t *buf) +{ + struct urb p; + mk_bulk(&p, EP_IN, buf, 9 * 1024 * 1024); + if (submit(&p) != 0) + return false; + io(USBDEVFS_DISCARDURB, &p); + (void) reap_within(2000); + return true; +} + +static void t_orphan_refund(void) +{ + enter("t_orphan_refund"); + quiesce(); + fx_reset(); + + /* The wedge's abort lands well past the 2 s deadline, so there is a wide + * window in which the record is orphaned and its buffer still IOKit's. + */ + fx_script("ep02:wedge(5000);ep81:never"); + uint8_t *nine = malloc(9 * 1024 * 1024); + uint8_t *eight = calloc(1, 8 * 1024 * 1024); + if (!nine || !eight) { + TEST("room for the budget probes"); + FAILF("malloc failed"); + free(nine); + free(eight); + return; + } + + /* 9 MiB fits in the 16 MiB allowance alone and does not fit beside 8 MiB, + * which is the whole question this scenario asks the budget. + */ + TEST("9 MiB fits while nothing else is charged"); + CHECK(budget_takes_9mb(nine), "SUBMITURB rc=%d", errno); + + struct urb w; + mk_bulk(&w, EP_OUT, eight, 8 * 1024 * 1024); + long sub = submit(&w); + TEST("an 8 MiB transfer that will not drain is accepted"); + CHECK(sub == 0, "SUBMITURB rc=%ld", sub); + TEST("and 9 MiB no longer fits beside it"); + CHECK(!budget_takes_9mb(nine), "accepted with 8 MiB charged"); + + struct setinterface si = {IFNUM, 0}; + double t0 = now_ms(); + long r = io(USBDEVFS_SETINTERFACE, &si); + double dt = now_ms() - t0; + TEST("SETINTERFACE is refused and the 8 MiB record is orphaned"); + CHECK(r == -EBUSY && dt >= 1900.0, "rc=%ld after %.0f ms", r, dt); + + TEST("the orphan keeps its charge: the buffer is still IOKit's"); + CHECK(!budget_takes_9mb(nine), "the budget came back before the memory"); + + /* usb_kill_urb hands every URB back, so an orphan is reapable with the + * status it leaves. It used to be unlinked, marked and dropped: the guest + * never got the pointer and CAP_REAP_AFTER_DISCONNECT, which + * GET_CAPABILITIES raises, did not hold on this path. + */ + struct urb *orph = reap_within(500); + TEST("the orphaned URB is handed back with usb_kill_urb's status"); + CHECK(orph == &w && w.status == -ENOENT, "d=%p status=%d", (void *) orph, + orph ? orph->status : 0); + + /* Handed back is not freed. The record carries an IN copy-out the reap + * reads and a buffer IOKit still owns, so the charge can only come back + * when the last of the two owners is done with it. + */ + TEST("and its charge is still held, the buffer being still IOKit's"); + CHECK(!budget_takes_9mb(nine), "the budget came back before the memory"); + + settle(3500); /* the late abort lands and frees the buffer */ + TEST("the charge comes back with the buffer, not before it"); + CHECK(budget_takes_9mb(nine), "SUBMITURB rc=%d", errno); + + free(nine); + free(eight); + quiesce(); +} + +/* A drain that misses its deadline still has to restart the FIFOs it shut. + * + * draining stops every endpoint, the ones the kill does not match included. A + * leader on such an endpoint can complete inside the drain window, and its + * callback starts nothing while the flag is up; the orphaned records' own late + * callbacks take the orphaned early return, so nothing else comes along to + * restart it. Without a kick on the timeout path that endpoint is left with a + * queued URB, none in flight, and a REAPURB that never returns. + */ +static void t_drain_timeout_restarts_fifos(void) +{ + enter("t_drain_timeout_restarts_fifos"); + quiesce(); + fx_reset(); + + /* ep0 belongs to no interface, so SETINTERFACE's per-interface kill does + * not match it and only draining stops it -- which is what leaves its + * follower with nothing to restart it. + */ + fx_script("ep0:delay(400),ok;ep02:wedge(4000)"); + uint8_t setup_l[8] = {0x40, 0x01, 0, 0, 0, 0, 0, 0}; + uint8_t setup_f[8] = {0x40, 0x02, 0, 0, 0, 0, 0, 0}; + uint8_t wbuf[8] = {0}; + struct urb ul, uf, uw; + memset(&ul, 0, sizeof ul); + ul.type = URB_TYPE_CONTROL; + ul.endpoint = 0; + ul.buffer = setup_l; + ul.buffer_length = sizeof setup_l; + memset(&uf, 0, sizeof uf); + uf.type = URB_TYPE_CONTROL; + uf.endpoint = 0; + uf.buffer = setup_f; + uf.buffer_length = sizeof setup_f; + mk_bulk(&uw, EP_OUT, wbuf, sizeof wbuf); + submit(&ul); + submit(&uf); /* queued behind the leader on ep0's FIFO */ + submit(&uw); + + struct setinterface si = {IFNUM, 0}; + double t0 = now_ms(); + long r = io(USBDEVFS_SETINTERFACE, &si); + double dt = now_ms() - t0; + TEST("SETINTERFACE is refused when the wedged URB misses the deadline"); + CHECK(r == -EBUSY && dt >= 1900.0, "rc=%ld after %.0f ms", r, dt); + + struct urb *d = reap_within(1500); + TEST("the ep0 leader that completed inside the drain is handed back"); + CHECK(d == &ul && ul.status == 0, "d=%p status=%d", (void *) d, + d ? d->status : 0); + d = reap_within(1500); + TEST("the wedged record the deadline gave up on is handed back too"); + CHECK(d == &uw && uw.status == -ENOENT, "d=%p status=%d", (void *) d, + d ? d->status : 0); + d = reap_within(2500); + TEST("and the follower it left queued is started, not stranded"); + CHECK(d == &uf && uf.status == 0, "d=%p status=%d", (void *) d, + d ? d->status : 0); + + settle(2500); /* the wedge's late abort frees the orphan */ + quiesce(); +} + +/* disc_drained is per slot, and usbdev_init zeroes the table once at startup: + * nothing else clears it, so a slot whose previous open drained after a + * disconnect must have the flag reset with the rest of the per-open async + * state. Two opens in a row land on the same slot -- the allocator takes the + * lowest free one -- which is what puts the second open behind the first's + * flag. + * + * The disconnect here is a follower IOKit refuses with NoDevice, not a + * terminate: it stamps this fd and leaves the device every later scenario still + * needs. + */ +static void t_disc_drained_reset(void) +{ + enter("t_disc_drained_reset"); + int prev = fd; + unsigned ifn = IFNUM; + for (int pass = 0; pass < 2; pass++) { + int f = open(NODE, O_RDWR); + if (f < 0) { + TEST("an fd on the loopback node"); + FAILF("open rc=%d", errno); + fd = prev; + return; + } + fd = f; + io(USBDEVFS_CLAIMINTERFACE, &ifn); + fx_reset(); + fx_script("ep81:never,refuse(0xe00002c0);ep83:never"); + + uint8_t a[16], b[16], c[16]; + struct urb ua, ub, uc; + mk_bulk(&ua, EP_IN, a, sizeof a); + mk_bulk(&ub, EP_IN, b, sizeof b); + memset(&uc, 0, sizeof uc); + uc.type = URB_TYPE_INTERRUPT; + uc.endpoint = EP_INT; + uc.buffer = c; + uc.buffer_length = sizeof c; + submit(&ua); + submit(&ub); /* queued behind the leader */ + submit(&uc); /* the one only the drain can hand back */ + + /* The leader reaps -ENOENT and its callback starts the follower, which + * IOKit refuses with NoDevice: that is what stamps the fd. + */ + io(USBDEVFS_DISCARDURB, &ua); + + int back = 0; + bool got_int = false; + for (int i = 0; i < 4; i++) { + struct urb *d = reap_within(2000); + if (!d) + break; + back++; + if (d == &uc) + got_int = true; + } + if (pass == 0) { + TEST("a fresh slot drains its disconnect and returns every URB"); + CHECK(back == 3 && got_int, "%d back, ep83's %s", back, + got_int ? "among them" : "lost"); + } else { + TEST("and so does the next open that lands on the same slot"); + CHECK(back == 3 && got_int, "%d back, ep83's %s", back, + got_int ? "among them" : "lost"); + } + close(f); + settle(50); /* the slot is free again before the next open */ + } + fd = prev; + + /* Each pass above stamped the whole device, this fd included, so the + * scenarios after this one need one that is not stamped. + */ + reopen_main(&prev); +} + +/* One vendor IN control transfer of a named length, straight through the + * synchronous CONTROL path. The fixture answers this one itself, so what the + * assertions below are about is the ioctl's own bookkeeping, not the wire. + */ +static long ctrl_of(int len) +{ + struct ctrltransfer ct = {.bRequestType = 0xc0, + .bRequest = FX_LOG, + .wValue = 0, + .wIndex = 0, + .wLength = (uint16_t) len, + .timeout = 1000, + .data = logbuf}; + return io(USBDEVFS_CONTROL, &ct); +} + +/* A synchronous CONTROL is charged against the in-flight allowance. + * + * do_proc_control books PAGE_SIZE + sizeof(struct urb) + sizeof(struct + * usb_ctrlrequest) before it touches its buffer and gives the same amount back + * afterwards (devio.c:1187 and :1269): a fixed charge, not the request's + * length. This path took none at all, so it was the one synchronous transfer + * outside a budget the async engine and the synchronous BULK both respect -- + * measured with 16771328 bytes in flight, a sync BULK answered ENOMEM and a + * sync CONTROL of wLength 4096 went through and reported 4096. + * + * The allowance is filled with URBs rather than with a transfer of its own, + * because a synchronous transfer's charge is gone again by the time its ioctl + * returns. Only the leader of an endpoint's FIFO reaches IOKit; the rest queue + * inside elfuse, and every one of them is charged at submit, so one never- + * completing leader is enough to park the whole budget. + */ +#define CHARGE_POOL 64 + +static void t_control_charge(void) +{ + enter("t_control_charge"); + fx_reset(); + fx_script("ep81:never"); + + uint8_t *big = malloc(4u << 20); + static struct urb pool[CHARGE_POOL]; + if (!big) { + TEST("a buffer to fill the allowance with"); + FAILF("malloc"); + return; + } + memset(big, 0, 4u << 20); + + /* Descending steps, so the last accepted URB leaves less headroom than any + * charge a CONTROL can take. The guest cannot name the record overhead the + * engine adds, and does not have to: what it needs is a full allowance. + */ + static const unsigned step[] = {4u << 20, 1u << 20, 64u << 10, + 4096, 256, 8}; + int n = 0; + for (unsigned si = 0; si < sizeof(step) / sizeof(step[0]); si++) { + while (n < CHARGE_POOL) { + mk_bulk(&pool[n], EP_IN, big, (int) step[si]); + if (submit(&pool[n]) != 0) + break; + n++; + } + } + TEST("the allowance filled with URBs that will not complete"); + CHECK(n > 0 && n < CHARGE_POOL, "%d URBs accepted", n); + + mk_bulk(&pool[n], EP_IN, big, 4096); + TEST("a further 4 KiB URB no longer fits"); + CHECK(submit(&pool[n]) == -ENOMEM, "submit rc=%d", errno); + + long cr = ctrl_of(4096); + TEST("and neither does a synchronous CONTROL of the same length"); + CHECK(cr == -ENOMEM, "CONTROL rc=%ld", cr); + + for (int i = 0; i < n; i++) + io(USBDEVFS_DISCARDURB, &pool[i]); + int back = 0; + for (int i = 0; i < n; i++) + if (reap_within(2000)) + back++; + TEST("every filler URB comes back"); + CHECK(back == n, "%d of %d", back, n); + + cr = ctrl_of(4096); + TEST("and the CONTROL goes through once the allowance is given back"); + CHECK(cr >= 0, "CONTROL rc=%ld", cr); + printf(" allowance held by %d URBs; CONTROL refused, then %ld\n", n, cr); + free(big); + quiesce(); +} + +/* A non-blocking reap never waits -- not even on the pass that owes the + * post-disconnect kill. + * + * Linux's proc_reapurbnonblock pops async_getcompleted and answers EAGAIN or + * ENODEV: it neither kills nor waits, because usbdev_remove ran + * destroy_all_async before any reap could see the disconnect. This engine owes + * that kill at the first reap that finds the completion list empty, and running + * the whole of it inline made REAPURBNDELAY sit out the drain's 2 s deadline + * holding async_lock against every SUBMITURB and DISCARDURB on the fd. Measured + * before the split: rc=-1 errno=19 after 2006 ms, and 0 ms on the pass after + * it. + * + * ep81 carries the URB the kill cannot recover promptly -- wedge answers its + * abort 2500 ms later, past the deadline -- and ep83 is only how the fd gets + * stamped without terminating the fixture's device, so this scenario can run + * before the one that does. + */ +static void t_reap_ndelay_never_waits(void) +{ + enter("t_reap_ndelay_never_waits"); + int prev = fd; + uint8_t a[16], b[16], c[16]; + struct urb ua, ub, uc; + fx_reset(); + + /* The wedge lands inside the drain deadline on purpose. Past it the + * deadline is what answers -- a non-blocking reap orphans the survivor and + * reports the device gone, which is the other half of this invariant and + * t_ndelay_reaches_enodev's subject. Here the question is only whether the + * pass that owes the kill waits, so the URB has to be one that genuinely + * comes back. + */ + fx_script("ep81:wedge(1200);ep83:never,refuse(0xe00002c0)"); + + mk_bulk(&ua, EP_IN, a, sizeof(a)); + submit(&ua); + + memset(&ub, 0, sizeof(ub)); + ub.type = URB_TYPE_INTERRUPT; + ub.endpoint = EP_INT; + ub.buffer = b; + ub.buffer_length = sizeof(b); + uc = ub; + uc.buffer = c; + submit(&ub); + submit(&uc); /* queued behind the leader */ + + /* The leader's abort completes it and starts the follower, which IOKit + * refuses with NoDevice: that refusal is the disconnect stamp. + */ + io(USBDEVFS_DISCARDURB, &ub); + + int back = 0; + for (int i = 0; i < 2; i++) + if (reap_within(2000)) + back++; + TEST("the stamped fd hands back the two ep83 URBs first"); + CHECK(back == 2, "%d of 2 back", back); + + /* Now the completion list is empty, the fd is disconnected and ep81's URB + * is still outstanding: this is the pass that owes the kill. + */ + double t0 = now_ms(); + struct urb *out = NULL; + long r = io(USBDEVFS_REAPURBNDELAY, &out); + double dt = now_ms() - t0; + + /* EAGAIN, not ENODEV: the aborts have gone out but ep81's URB has not come + * back, and ENODEV is the end of the conversation. proc_reapurbnonblock's + * own two answers are EAGAIN and ENODEV, and the one it would give if it + * could reach this state -- disconnected, URBs still out -- is EAGAIN, + * because on Linux destroy_all_async has already emptied the pending list + * before any reap sees the disconnect. + */ + TEST("the reap that owes the kill answers EAGAIN, not ENODEV"); + CHECK(r == -EAGAIN, "rc=%ld", r); + TEST("and answers it without waiting out the 2 s drain deadline"); + CHECK(dt < 500.0, "elapsed %.0f ms", dt); + printf(" REAPURBNDELAY on the pass that owed the kill: %.0f ms\n", dt); + + /* The aborts were issued, only not waited for, so the wedged URB comes back + * on its own timetable -- and it comes back before any ENODEV does. + */ + settle(1400); + struct urb *late = NULL; + long lr = io(USBDEVFS_REAPURBNDELAY, &late); + TEST("the URB the aborts recovered is handed back once it lands"); + CHECK(lr == 0 && late == &ua, "rc=%ld urb=%p", lr, (void *) late); + TEST("and only then is the fd out of URBs to answer for"); + CHECK(io(USBDEVFS_REAPURBNDELAY, &out) == -ENODEV, "rc=%d", errno); + quiesce(); + reopen_main(&prev); +} + +/* CAP_REAP_AFTER_DISCONNECT as one invariant, against all three reap orderings: + * after a disconnect every in-flight URB is handed back BEFORE any reap answers + * ENODEV, whichever flavor asked first. + * + * The regression this pins was a single flag doing two jobs. disc_drained is a + * one-shot for issuing the post-disconnect aborts; it was also read as + * permission to report the device drained, so a REAPURBNDELAY -- which issues + * the aborts and by contract cannot wait for them -- latched it and every later + * reap, blocking included, answered ENODEV with the URBs still in flight. An + * ordinary libusb event loop polls non-blocking before it blocks, so pass 2 + * below is the common path and not a corner: measured before the fix as + * REAPURBNDELAY rc=-19 urb=(nil) and then REAPURB rc=-19 in 0 ms, with the URB + * arriving 400 ms after both. + * + * ep81 carries the two URBs whose refusal stamps the fd without terminating the + * fixture's device, so this can run before the scenario that does. ep83 carries + * the one only the drain can recover: wedge(300) answers its abort late enough + * that the window is real, and early enough to stay inside the 2 s deadline, so + * the URB genuinely comes back rather than being orphaned. + */ +static void t_enodev_never_precedes_the_urbs(void) +{ + enter("t_enodev_never_precedes_the_urbs"); + int prev = fd; + static const char *const what[3] = { + "every reap non-blocking", + "every reap blocking", + "a non-blocking poll first, then a blocking reap", + }; + for (int pass = 0; pass < 3; pass++) { + /* Pass 2 lets its one non-blocking probe answer what it likes -- pass 0 + * is what holds that answer to EAGAIN -- and asks the narrower question + * the regression was measured on: whether the BLOCKING reap behind the + * probe still waits for the URB, or takes the probe's latch as + * permission to answer ENODEV in no time at all. + */ + bool tolerate_probe = pass == 2; + uint8_t a[16], b[16], c[16]; + struct urb ua, ub, uc; + fx_reset(); + fx_script("ep81:never,refuse(0xe00002c0);ep83:wedge(300)"); + + mk_bulk(&ua, EP_IN, a, sizeof(a)); + mk_bulk(&ub, EP_IN, b, sizeof(b)); + submit(&ua); + submit(&ub); /* queued behind the leader */ + memset(&uc, 0, sizeof(uc)); + uc.type = URB_TYPE_INTERRUPT; + uc.endpoint = EP_INT; + uc.buffer = c; + uc.buffer_length = sizeof(c); + submit(&uc); /* the one only the drain can hand back */ + + /* The leader's abort completes it and starts the follower, which IOKit + * refuses with NoDevice: that refusal is the disconnect stamp. + */ + io(USBDEVFS_DISCARDURB, &ua); + + /* probes counts the non-blocking reaps taken once the two completions + * have run out. Pass 2 takes exactly one -- the pass that owes the + * kill, which issues the aborts and latches disc_drained -- and blocks + * from then on, which is the ordering a libusb event loop produces and + * the one the regression turned into an instant ENODEV. + */ + int back = 0, probes = 0; + long early = 0; + double blocked_ms = -1.0; + for (int i = 0; i < 400 && back < 3 && !early; i++) { + struct urb *out = NULL; + bool blocking = pass == 1 || (pass == 2 && probes >= 1); + double t0 = now_ms(); + long r = + io(blocking ? USBDEVFS_REAPURB : USBDEVFS_REAPURBNDELAY, &out); + if (r == 0) { + back++; + continue; + } + if (r == -ENODEV && !(tolerate_probe && !blocking)) { + early = 1; /* the regression: ENODEV before the URBs */ + if (blocking) + blocked_ms = now_ms() - t0; + continue; + } + if (back >= 2) + probes++; + settle(5); + } + TEST(what[pass]); + if (early && blocked_ms >= 0) + FAILF("the blocking reap answered ENODEV in %.0f ms, %d of 3 back", + blocked_ms, back); + else + CHECK(!early && back == 3, "%s after %d of 3 URBs", + early ? "ENODEV" : "gave up", back); + + /* Having answered for all three, the fd may end the conversation. */ + struct urb *out = NULL; + TEST("and ENODEV once every URB has been answered for"); + CHECK(io(USBDEVFS_REAPURB, &out) == -ENODEV, "rc=%d", errno); + reopen_main(&prev); + } + fd = prev; +} + +/* The other half of CAP_REAP_AFTER_DISCONNECT: after a disconnect every reap + * flavor reaches the end of the conversation, and reaches it inside one drain + * deadline of the aborts. + * + * The regression this pins is the fix above growing a hole underneath it. + * Splitting -ENODEV off the disc_drained latch made "nothing left out" the + * whole of the -ENODEV condition, and the only thing that emptied the pending + * list on a wire that answers no abort was the BLOCKING arm's 2 s wait and its + * orphaning. A REAPURBNDELAY loop -- which is what an ordinary libusb event + * loop runs -- had no such ceiling: it issued the aborts on its first pass and + * could only answer -EAGAIN from then on, for ever, while poll held + * POLLERR|POLLHUP. Measured before the fix with ep81:wedge(600000): 4000 ms of + * REAPURBNDELAY gave 1958386 -EAGAIN and no -ENODEV, with revents 0x18, where + * Linux's proc_reapurbnonblock answers connected(ps) ? -EAGAIN : -ENODEV and + * the guest tears down. + * + * ep81:wedge(4000) is a wire that answers its abort well past the deadline, so + * what decides here is the deadline and not the wire; the ep83 refusal is the + * disconnect stamp, and it leaves no record behind. + */ +static void t_ndelay_reaches_enodev(void) +{ + enter("t_ndelay_reaches_enodev"); + int prev = fd; + uint8_t a[16], c[16]; + struct urb ua, uc; + fx_reset(); + fx_script("ep81:wedge(4000);ep83:refuse(0xe00002c0)"); + + mk_bulk(&ua, EP_IN, a, sizeof(a)); + submit(&ua); + memset(&uc, 0, sizeof(uc)); + uc.type = URB_TYPE_INTERRUPT; + uc.endpoint = EP_INT; + uc.buffer = c; + uc.buffer_length = sizeof(c); + TEST("the refused submit stamps the fd"); + CHECK(submit(&uc) == -ENODEV, "SUBMITURB rc=%d", errno); + + /* Before the deadline the URB may still come back, so -EAGAIN is the only + * honest answer and orphaning early would throw it away. + */ + double t0 = now_ms(); + struct urb *out = NULL; + long r = io(USBDEVFS_REAPURBNDELAY, &out); + double owed_ms = now_ms() - t0; + TEST("the pass that owes the kill answers EAGAIN"); + CHECK(r == -EAGAIN, "rc=%ld", r); + TEST("and issues the aborts without waiting for them"); + CHECK(owed_ms < 100.0, "elapsed %.0f ms", owed_ms); + settle(500); + r = io(USBDEVFS_REAPURBNDELAY, &out); + TEST("and still EAGAIN half a second in, with the deadline unspent"); + CHECK(r == -EAGAIN, "rc=%ld", r); + + /* Spin the way libusb_handle_events_timeout(0) does. Bounded well past the + * deadline and well inside the wedge's own abort, so an -ENODEV here is the + * deadline's answer and a timeout here is the unbounded spin. + */ + long eagain = 0; + int handed_back = 0; + double elapsed = -1.0, call_ms = -1.0; + while (now_ms() - t0 < 3500.0) { + double c0 = now_ms(); + out = NULL; + r = io(USBDEVFS_REAPURBNDELAY, &out); + if (r == -ENODEV) { + call_ms = now_ms() - c0; + elapsed = now_ms() - t0; + break; + } + if (r == 0 && out == &ua) + handed_back++; + if (r == -EAGAIN) + eagain++; + } + + /* The whole of CAP_REAP_AFTER_DISCONNECT, on the one path where it did not + * hold: the deadline's give-up used to unlink the record, mark it and drop + * it, so the pass that gave up answered -ENODEV with the URB pointer never + * returned -- measured as -ENODEV after 2037 ms with 0 URBs handed back, + * against a control script that does hand one back. Linux's + * destroy_all_async completes every pending URB into async_completed first, + * and usb_kill_urb leaves -ENOENT in each. + */ + TEST("the wedged URB is handed back before the conversation ends"); + CHECK(handed_back == 1 && ua.status == -ENOENT, "%d back, status=%d", + handed_back, ua.status); + TEST("a REAPURBNDELAY loop reaches ENODEV rather than spinning for ever"); + CHECK(elapsed >= 0, "%ld EAGAIN and no ENODEV in %.0f ms", eagain, + now_ms() - t0); + if (elapsed >= 0) { + /* The deadline is armed by the pass that issues the aborts, a hair + * after t0, so this brackets 2000 ms from below rather than demanding + * it: the point is that neither the wire's 4000 ms abort nor an + * immediate give-up is what answered. + */ + TEST("it is the drain deadline that decides, not the wire"); + CHECK(elapsed >= 1900.0 && elapsed < 2500.0, "ENODEV at %.0f ms", + elapsed); + + /* An entry whose contract says it does not block has to measure the + * deadline rather than wait it out: the deadline is spent by the + * caller's own spinning, never inside one call. + */ + TEST("and no single REAPURBNDELAY blocked to get there"); + CHECK(call_ms < 100.0, "the ENODEV call took %.0f ms", call_ms); + printf( + " REAPURBNDELAY reached ENODEV at %.0f ms, longest call %.0f ms\n", + elapsed, call_ms); + } + + /* The blocking flavor behind it agrees and does not wait either: there is + * nothing left out for it to wait for. + */ + double b0 = now_ms(); + long br = io(USBDEVFS_REAPURB, &out); + double bms = now_ms() - b0; + TEST("a blocking REAPURB behind it answers ENODEV without waiting"); + CHECK(br == -ENODEV && bms < 100.0, "rc=%ld in %.0f ms", br, bms); + + TEST("nothing is left to reap, and ERR|HUP stays raised"); + CHECK(poll_once(POLLOUT | POLLWRNORM, 50) == (POLLERR | POLLHUP), + "revents=0x%x", poll_once(POLLOUT | POLLWRNORM, 0)); + + settle(2600); /* the wedge's late abort frees the orphaned record */ + quiesce(); + reopen_main(&prev); +} + +/* DISCARDURB's own abort is an op on the device, and what it answers is news. + * + * Both aborts here used to be cast to void. An AbortPipe that comes back + * kIOReturnNoDevice canceled nothing, so the record stayed URB_INFLIGHT in the + * pending list and the wait below it sat out its whole two seconds before + * returning 0; the fd was never stamped, although usbdev_arm_disconnect_watch + * names kIOReturnNoDevice detection on ops as its fallback and this is such an + * op; and the URB was never handed back, although proc_unlinkurb is + * usb_kill_urb, which always leaves the URB reapable. Measured: rc=0 after 2006 + * ms, fd unstamped, the following REAPURB still unanswered when the probe was + * killed at 20 s. + * + * Nothing in the fixture's vocabulary could produce that answer -- AbortPipe + * landed unconditionally -- so the fixture learned one fact of its own for it, + * beside the pipes_gone it already had. Tying aborts to the terminate instead + * would have left every 'never' transfer outstanding for ever, which is the + * reason the seam invariant lists AbortPipe as deliberately outside it. + */ +static void t_discard_when_the_abort_is_refused(void) +{ + enter("t_discard_when_the_abort_is_refused"); + int prev = fd; + quiesce(); + fx_reset(); + fx_script("ep02:never"); + uint8_t out[8] = {0}; + struct urb u; + mk_bulk(&u, EP_OUT, out, sizeof(out)); + long sub = submit(&u); + TEST("a transfer the wire will not answer on its own is accepted"); + CHECK(sub == 0, "SUBMITURB rc=%ld", sub); + + TEST("the fixture can refuse an abort the way a departed user client does"); + CHECK(fx_aborts_gone() == 0, "rc=%d", errno); + + double t0 = now_ms(); + long r = io(USBDEVFS_DISCARDURB, &u); + double dt = now_ms() - t0; + TEST("DISCARDURB answers at once rather than waiting out its own deadline"); + CHECK(r == 0 && dt < 500.0, "rc=%ld after %.0f ms", r, dt); + + int rev = poll_once(POLLIN, 0); + TEST("the refusal stamps the fd, as every other device-gone answer does"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + rev); + + struct urb *d = reap_within(500); + TEST("and the URB comes back: proc_unlinkurb is usb_kill_urb"); + CHECK(d == &u && u.status == -ENOENT, "d=%p status=%d", (void *) d, + d ? d->status : 0); + + reopen_main(&prev); + fx_reset(); /* the abort refusal is a fixture fact, not this fd's */ +} + +/* An orphaned ep0 record pins the device handle, which nothing used to. + * + * usbdev_orphan_stalled_locked pins the handle an orphan still references so + * teardown will not release it, and the pin was guarded on the record naming an + * interface -- so a record on the default control pipe, which names none, + * pinned nothing. The whole-slot drain unlinks it from pending, so the rescan + * inside usbdev_kill_urbs_locked comes back clean, teardown reads drained and + * releases u->dev with a DeviceRequestAsyncTO still outstanding at IOKit. + * Measured as close() printing no leaking-handle line where the same scenario + * on an interface endpoint prints one. + * + * The use-after-free that costs on real IOKit is out of reach here -- the + * fixture frees a COM wrapper nothing dereferences again -- so what the fixture + * counts instead is the release itself, against a transfer it still owns. That + * counter is read back on a later fd, because the release happens at close. + * + * SETCONFIGURATION is the way in: it kills every URB on the device, ep0 + * included, and Linux refuses it while any interface is claimed, so the claim + * goes first and the queue left is exactly the one no claim covers. + */ +static void t_ep0_orphan_pins_the_device(void) +{ + enter("t_ep0_orphan_pins_the_device"); + unsigned ifn = IFNUM; + quiesce(); + fx_reset(); + io(USBDEVFS_RELEASEINTERFACE, &ifn); + fx_script("ep0:wedge(3000)"); + + uint8_t setup[8] = {0x40, 0x01, 0, 0, 0, 0, 0, 0}; + struct urb u; + memset(&u, 0, sizeof(u)); + u.type = URB_TYPE_CONTROL; + u.endpoint = 0; + u.buffer = setup; + u.buffer_length = sizeof(setup); + long sub = submit(&u); + TEST("a control URB on ep0 is accepted with no interface claimed"); + CHECK(sub == 0, "SUBMITURB rc=%ld", sub); + + unsigned cfg = 1; + long r = io(USBDEVFS_SETCONFIGURATION, &cfg); + TEST("SETCONFIGURATION is refused and the ep0 record is orphaned"); + CHECK(r == -EBUSY, "rc=%ld", r); + struct urb *d = reap_within(500); + TEST("the orphaned ep0 URB is handed back like any other"); + CHECK(d == &u && u.status == -ENOENT, "d=%p status=%d", (void *) d, + d ? d->status : 0); + + close(fd); + fd = open(NODE, O_RDWR); + if (fd < 0) { + TEST("a fresh fd after the teardown that would have leaked"); + FAILF("open rc=%d", errno); + return; + } + uint32_t leaked = fx_dev_releases_in_flight(); + TEST("teardown did not release the device handle under the ep0 transfer"); + CHECK(leaked == 0, "%u release(s) with a transfer still outstanding", + leaked); + + settle(1500); /* the wedge's late abort lands and frees the orphan */ + fx_reset(); + io(USBDEVFS_CLAIMINTERFACE, &ifn); +} + +/* A pipe map that cannot be read is a device-gone answer, not an interface with + * fewer endpoints than it has. + * + * GetNumEndpoints sizes the map and GetPipeProperties fills it in, and the + * device can go between the two. The failing call used to be a continue, so + * CLAIMINTERFACE answered 0 with npipes short of the interface's real count and + * left the fd unstamped: the guest got a claimed interface whose SUBMITURB then + * answered -ENOENT, no such endpoint, where Linux answers -ENODEV. Only + * GetPipeProperties goes gone here, which is exactly that window. + */ +static void t_pipe_map_device_gone(void) +{ + enter("t_pipe_map_device_gone"); + int prev = fd; + unsigned ifn = IFNUM; + fx_reset(); + io(USBDEVFS_RELEASEINTERFACE, &ifn); + TEST("the pipes-gone command is accepted"); + CHECK(fx_pipes_gone() == 0, "rc=%d", errno); + + long r = io(USBDEVFS_CLAIMINTERFACE, &ifn); + TEST("a claim whose pipe map cannot be read answers ENODEV"); + CHECK(r == -ENODEV, "claim rc=%ld", r); + + /* And the answer was originated, not just returned: the connected() gate in + * usbdev_ioctl is what answers from here on. + */ + uint8_t a[16]; + struct urb ua; + mk_bulk(&ua, EP_IN, a, sizeof(a)); + TEST("and it stamped the fd, so the next ioctl agrees"); + CHECK(submit(&ua) == -ENODEV, "SUBMITURB rc=%d", errno); + + /* Put the pipes back for the scenarios after this one. The reset is a + * control request, and this fd is stamped now, so its own connected() gate + * would answer it -ENODEV: it has to go over an fd that is not. + */ + int tmp = open(NODE, O_RDWR); + if (tmp >= 0) { + int save = fd; + fd = tmp; + TEST("the pipes come back over an fd the disconnect did not stamp"); + CHECK(fx_reset() == 0, "reset rc=%d", errno); + fd = save; + close(tmp); + } + reopen_main(&prev); +} + +/* Last: the device never comes back. */ +static void t_disconnect(void) +{ + enter("t_disconnect"); + uint8_t a[32], b[32], z[32]; + struct urb ua, ub, uz; + fx_reset(); + fx_script("ep02:ok;ep81:never;ep83:never"); + + /* One completion the guest has not collected yet, and two URBs still on the + * wire. The unreaped one is the whole point: the drain is decided once per + * fd, and a pass that has a completion to hand back returns before it can + * issue the kill, so deciding it on such a pass claims the drain and never + * performs it -- these two would then never come back. + */ + mk_bulk(&uz, EP_OUT, z, sizeof(z)); + submit(&uz); + for (int i = 0; i < 200 && poll_once(POLLOUT | POLLWRNORM, 10) == 0; i++) + ; + + mk_bulk(&ua, EP_IN, a, sizeof(a)); + memset(&ub, 0, sizeof(ub)); + ub.type = URB_TYPE_INTERRUPT; + ub.endpoint = EP_INT; + ub.buffer = b; + ub.buffer_length = sizeof(b); + submit(&ua); + submit(&ub); + TEST("the terminate command is accepted"); + CHECK(fx_terminate(0) == 0, "terminate rc=%d", errno); + + /* Unmaskable: the guest asked for POLLIN, which this fd never raises. */ + int rev = 0; + for (int i = 0; i < 100 && !rev; i++) + rev = poll_once(POLLIN, 50); + TEST("a disconnect reports POLLERR|POLLHUP through a POLLIN-only wait"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + rev); + + int back = 0, enoent = 0; + struct urb *first = NULL; + for (int i = 0; i < 5; i++) { + struct urb *d = reap_within(2000); + if (!d) + break; + if (!back) + first = d; + back++; + if (d->status == -ENOENT) + enoent++; + } + TEST("the completion left unreaped at the disconnect comes back first"); + CHECK(first == &uz && uz.status == 0, "first=%p status=%d", (void *) first, + first ? first->status : 0); + TEST("CAP_REAP_AFTER_DISCONNECT hands back every in-flight URB"); + CHECK(back == 3, "reaped %d of 3", back); + TEST("each in-flight one carries the errno usb_kill_urb leaves"); + CHECK(enoent == 2, "%d of %d were -ENOENT", enoent, back); + struct urb *out = NULL; + TEST("REAPURB is -ENODEV once the drain is done"); + CHECK(io(USBDEVFS_REAPURB, &out) == -ENODEV, "rc=%d", errno); + + /* The disconnect wake byte keeps the completion pipe readable for good, so + * the reapable half of the answer has to come from the completed list and + * not from the pipe: with nothing left to hand back, POLLOUT|POLLWRNORM + * must be gone while the unmaskable pair stays. + */ + int quiet = poll_once(POLLOUT | POLLWRNORM, 50); + TEST("a disconnect with nothing left to reap reports ERR|HUP alone"); + CHECK(quiet == (POLLERR | POLLHUP), "revents=0x%x", quiet); + + /* select() counts a descriptor once per set it is reported in, not once per + * descriptor: fs/select.c has EPOLLERR in both POLLIN_SET and POLLOUT_SET + * and does retval++ for each. A disconnected usbfs fd asked about in both + * sets is therefore 2, where the host saw only the completion pipe in its + * own READ set and answered 1. + */ + fd_set rs, ws; + FD_ZERO(&rs); + FD_ZERO(&ws); + FD_SET(fd, &rs); + FD_SET(fd, &ws); + struct timeval tv = {0, 0}; + int sr = select(fd + 1, &rs, &ws, NULL, &tv); + int rbit = FD_ISSET(fd, &rs) ? 1 : 0, wbit = FD_ISSET(fd, &ws) ? 1 : 0; + TEST("select counts a disconnected fd once per set it was asked about"); + CHECK(sr == 2 && rbit && wbit, "ret=%d read=%d write=%d", sr, rbit, wbit); +} + +/* A synchronous op has to ORIGINATE the disconnect, not merely report it. + * + * The peer walk publishes a disconnect to every other fd on the node, but + * something has to raise one first, and every site that did was the async + * engine's: usbdev_arm_disconnect_watch is reached only from + * usbdev_ensure_dev_async and usbdev_ensure_iface_async, both on the SUBMITURB + * path, and the kIOReturnNoDevice stamps were all in the completion callback + * and the URB start. An fd whose only contact with a departed device is a + * synchronous transfer therefore answered ENODEV from that one ioctl and went + * on reporting itself healthy to everything else: measured as poll revents + * 0x0000 and GET_CAPABILITIES 0 on a device whose own CONTROL, on that same fd, + * had just answered -19. Routing every op's IOKit status through + * usbdev_ioret_op is what makes the header's "any op" true. + * + * Runs after t_disconnect, so the fixture's device is already terminated and a + * fresh fd on the node meets one that is gone. Neither fd here submits a URB, + * so no terminate watch is armed on either and the walk has no other way to + * reach them: the synchronous CONTROL is the only possible source of the news. + */ +static void t_sync_op_originates_disconnect(void) +{ + enter("t_sync_op_originates_disconnect"); + int prev = fd; + int fa = open(NODE, O_RDWR); + int fb = open(NODE, O_RDWR); + if (fa < 0 || fb < 0) { + TEST("two fresh fds on the terminated loopback node"); + FAILF("open rc=%d", errno); + if (fa >= 0) + close(fa); + if (fb >= 0) + close(fb); + return; + } + int rev = poll_fd_once(fb, POLLIN | POLLOUT | POLLWRNORM, 0); + TEST("a fresh fd on a departed device starts out undisconnected"); + CHECK(rev == 0, "revents=0x%x", rev); + + /* Not one of the fixture's command requests (0xf0-0xf3): those are answered + * ahead of its terminated check, so they would never reach it. + */ + uint8_t buf[8]; + struct ctrltransfer ct = {.bRequestType = 0xc0, + .bRequest = 0x10, + .wValue = 0, + .wIndex = 0, + .wLength = sizeof(buf), + .timeout = 1000, + .data = buf}; + fd = fb; + long cr = io(USBDEVFS_CONTROL, &ct); + TEST("its synchronous CONTROL answers ENODEV"); + CHECK(cr == -ENODEV, "rc=%ld", cr); + + rev = poll_fd_once(fb, POLLIN, 0); + TEST("and that answer stamps the fd that asked"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + rev); + + uint32_t caps = 0; + TEST("so the fd's other ioctls answer ENODEV rather than succeeding"); + CHECK(io(USBDEVFS_GET_CAPABILITIES, &caps) == -ENODEV, "rc=%d", errno); + + rev = poll_fd_once(fa, POLLIN, 0); + TEST("and the peer that never touched the device is stamped with it"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + rev); + + close(fa); + close(fb); + fd = prev; +} + +/* The ops that ask the device about its interfaces answer for the device, not + * for the interface. + * + * All four reach it through one CreateInterfaceIterator, whose IOReturn used to + * be discarded into IO_OBJECT_NULL -- indistinguishable from "the enumeration + * ran and this interface is not in it". So on a departed device GETDRIVER + * answered -ENODATA and the three claim/connect ops answered -EINVAL, each + * naming a missing interface on a device that was missing entirely, and none of + * them stamped the fd, so the ioctl after them answered wrongly too. Linux's + * usbdev_do_ioctl runs its connected() gate before any of these and answers + * -ENODEV. + * + * A fresh fd on the terminated device is what makes this reachable: nothing has + * stamped it yet, so usbdev_ioctl's own gate lets the call through to the wire + * -- the same window t_sync_op_originates_disconnect uses for CONTROL. One fd + * per op, because the first answer stamps the whole device. + */ +static void t_iface_query_on_a_departed_device(void) +{ + enter("t_iface_query_on_a_departed_device"); + int prev = fd; + + /* Not IFNUM: the main fd holds that one, and usbfs is one driver + * device-wide, so all four ops answer from the claim without asking the + * device anything. An unclaimed interface number is what reaches the + * enumeration. + */ + const unsigned free_ifnum = 0; + struct getdriver gd = {.interface = free_ifnum}; + struct disconnect_claim dc = {.interface = free_ifnum}; + struct usbdevfs_ioctl idis = {.ifno = (int) free_ifnum, + .ioctl_code = USBDEVFS_IOCTL_DISCONNECT}; + struct usbdevfs_ioctl icon = {.ifno = (int) free_ifnum, + .ioctl_code = USBDEVFS_IOCTL_CONNECT}; + static const char *const what[4] = { + "GETDRIVER", + "DISCONNECT_CLAIM", + "USBDEVFS_IOCTL DISCONNECT", + "USBDEVFS_IOCTL CONNECT", + }; + const unsigned long req[4] = {USBDEVFS_GETDRIVER, USBDEVFS_DISCONNECT_CLAIM, + USBDEVFS_IOCTL, USBDEVFS_IOCTL}; + void *const arg[4] = {&gd, &dc, &idis, &icon}; + + for (int i = 0; i < 4; i++) { + int f = open(NODE, O_RDWR); + if (f < 0) { + TEST("a fresh fd on the terminated loopback node"); + FAILF("open rc=%d", errno); + fd = prev; + return; + } + fd = f; + long r = io(req[i], arg[i]); + TEST(what[i]); + if (r != -ENODEV) + FAILF("%s on a departed device rc=%ld, want ENODEV", what[i], r); + else + PASS(); + + /* Originated, not merely returned: the stamp is what the ops after it + * answer from, and its absence was half of this defect. + */ + int rev = poll_fd_once(f, POLLIN, 0); + TEST("and the answer stamped the fd that asked"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), + "%s: revents=0x%x", what[i], rev); + close(f); + } + fd = prev; +} + +/* The same four ops on an interface that IS claimed, which is the branch that + * answers out of this layer's own bookkeeping. + * + * A claim is elfuse state, not device state: it stays true after the device + * leaves, and the usbfs answer built from it puts no question to IOKit. So + * GETDRIVER answered 0 with driver "usbfs", the two USBDEVFS_IOCTL codes + * answered -EBUSY, and DISCONNECT_CLAIM answered 0 -- taking a fresh claim on + * an interface of a device that was gone -- each of them leaving the fd + * unstamped, so poll stayed silent and every ioctl after them answered from the + * stale slot too. Linux answers -ENODEV for all four: usbdev_do_ioctl's + * connected() gate runs before proc_getdriver, proc_disconnect_claim and + * proc_ioctl look at an interface at all. + * + * IFNUM rather than a free number, the opposite of the test above: the main fd + * holds it, so usbdev_iface_claimed_elsewhere is what answers and the + * enumeration is never reached. One fresh fd per op, because the first answer + * stamps the whole device. + */ +static void t_iface_query_answers_for_the_device_not_the_claim(void) +{ + enter("t_iface_query_answers_for_the_device_not_the_claim"); + int prev = fd; + + struct getdriver gd = {.interface = IFNUM}; + struct disconnect_claim dc = {.interface = IFNUM}; + struct usbdevfs_ioctl idis = {.ifno = IFNUM, + .ioctl_code = USBDEVFS_IOCTL_DISCONNECT}; + struct usbdevfs_ioctl icon = {.ifno = IFNUM, + .ioctl_code = USBDEVFS_IOCTL_CONNECT}; + static const char *const what[4] = { + "GETDRIVER", + "DISCONNECT_CLAIM", + "USBDEVFS_IOCTL DISCONNECT", + "USBDEVFS_IOCTL CONNECT", + }; + const unsigned long req[4] = {USBDEVFS_GETDRIVER, USBDEVFS_DISCONNECT_CLAIM, + USBDEVFS_IOCTL, USBDEVFS_IOCTL}; + void *const arg[4] = {&gd, &dc, &idis, &icon}; + + for (int i = 0; i < 4; i++) { + int f = open(NODE, O_RDWR); + if (f < 0) { + TEST("a fresh fd on the terminated loopback node"); + FAILF("open rc=%d", errno); + fd = prev; + return; + } + fd = f; + long r = io(req[i], arg[i]); + TEST(what[i]); + if (r != -ENODEV) + FAILF( + "%s on a claimed interface of a departed device rc=%ld, " + "want ENODEV", + what[i], r); + else + PASS(); + + int rev = poll_fd_once(f, POLLIN, 0); + TEST("and the answer stamped the fd that asked"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), + "%s: revents=0x%x", what[i], rev); + close(f); + } + fd = prev; +} + + +/* The device question runs before the interface number is judged, not after. + * + * GETDRIVER put usbdev_ensure_dev_reachable ahead of its range check and the + * other two put it behind theirs, so one departed device gave three different + * answers to the same probe: measured, GETDRIVER ifnum 200 was -ENODEV with the + * fd stamped 0x18, while DISCONNECT_CLAIM ifnum 200, USBDEVFS_IOCTL ifno 200 + * and ifno -1 were all -EINVAL with revents 0x0. Linux runs usbdev_do_ioctl's + * connected() gate before proc_disconnect_claim and proc_ioctl are entered at + * all, so the interface-number check inside them is behind it and every one of + * the four is -ENODEV. + * + * The number is out of range on purpose: an in-range one reaches the + * enumeration and is answered correctly either way, which is why the two + * scenarios above this one pass on the unfixed engine and this one does not. + * + * libusb_detach_kernel_driver(h, n) passes the application's own n through, so + * this is what an app that has not validated n against the config descriptor + * sees on an unplug: LIBUSB_ERROR_INVALID_PARAM instead of NO_DEVICE, and a + * poll on the fd that still reports nothing. + */ +static void t_out_of_range_ifnum_on_a_departed_device(void) +{ + enter("t_out_of_range_ifnum_on_a_departed_device"); + int prev = fd; + + const unsigned bad = 200; + struct getdriver gd = {.interface = bad}; + struct disconnect_claim dc = {.interface = bad}; + struct usbdevfs_ioctl ibig = {.ifno = (int) bad, + .ioctl_code = USBDEVFS_IOCTL_DISCONNECT}; + struct usbdevfs_ioctl ineg = {.ifno = -1, + .ioctl_code = USBDEVFS_IOCTL_DISCONNECT}; + static const char *const what[4] = { + "GETDRIVER ifnum 200", + "DISCONNECT_CLAIM ifnum 200", + "USBDEVFS_IOCTL ifno 200", + "USBDEVFS_IOCTL ifno -1", + }; + const unsigned long req[4] = {USBDEVFS_GETDRIVER, USBDEVFS_DISCONNECT_CLAIM, + USBDEVFS_IOCTL, USBDEVFS_IOCTL}; + void *const arg[4] = {&gd, &dc, &ibig, &ineg}; + + for (int i = 0; i < 4; i++) { + int f = open(NODE, O_RDWR); + if (f < 0) { + TEST("a fresh fd on the terminated loopback node"); + FAILF("open rc=%d", errno); + fd = prev; + return; + } + fd = f; + long r = io(req[i], arg[i]); + TEST(what[i]); + if (r != -ENODEV) + FAILF("%s on a departed device rc=%ld, want ENODEV", what[i], r); + else + PASS(); + int rev = poll_fd_once(f, POLLIN, 0); + TEST("and the answer stamped the fd that asked"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), + "%s: revents=0x%x", what[i], rev); + close(f); + } + fd = prev; +} + +/* A claim is granted only against a device the call has just reached. + * + * The scenarios from here down predate tests/test-usbdev-ioctl-departed.c and + * are kept for the shapes it does not build: an interface another fd holds, an + * interface number past the table, and a claim taken through usbdev_pipe_for_ep + * rather than by name. Which ops answer what on a departed device is that + * lane's table, not a count in any of these comments. + * + * usbdev_claim_locked asked usbdev_ensure_dev_plugin, which hands back a cached + * handle and puts no question to IOKit, so the claim went through on a device + * that had gone: measured, CLAIMINTERFACE 2 returned 0 with revents 0x0, and + * then RELEASEINTERFACE on the claim it had just invented returned 0 too. Linux + * answers -ENODEV to both. + * + * The three ops after it are the same defect through usbdev_pipe_for_ep, which + * claims the owning interface implicitly: CLEAR_HALT, RESETEP and SETINTERFACE + * all reached the wire calls behind that invented claim and returned 0. RESET + * is the fourth shape and needs no claim at all -- with none held its pipe loop + * runs zero IOKit calls, so it ran to the end and returned 0 on a device that + * was gone, which is libusb_reset_device, the canonical recovery call, + * reporting success to an application whose NO_DEVICE branch is the one it + * wanted. + * + * One fresh fd per op, because the first answer stamps the whole device. + */ +static void t_ops_that_reach_the_wire_ask_the_device_first(void) +{ + enter("t_ops_that_reach_the_wire_ask_the_device_first"); + int prev = fd; + + unsigned ifn = IFNUM; + unsigned ep_in = EP_IN, ep_out = EP_OUT; + struct setinterface si = {.interface = IFNUM, .altsetting = 0}; + static const char *const what[5] = { + "CLAIMINTERFACE", "CLEAR_HALT", "RESETEP", "SETINTERFACE", "RESET", + }; + const unsigned long req[5] = {USBDEVFS_CLAIMINTERFACE, USBDEVFS_CLEAR_HALT, + USBDEVFS_RESETEP, USBDEVFS_SETINTERFACE, + USBDEVFS_RESET}; + void *const arg[5] = {&ifn, &ep_in, &ep_out, &si, NULL}; + + for (int i = 0; i < 5; i++) { + int f = open(NODE, O_RDWR); + if (f < 0) { + TEST("a fresh fd on the terminated loopback node"); + FAILF("open rc=%d", errno); + fd = prev; + return; + } + fd = f; + long r = io(req[i], arg[i]); + TEST(what[i]); + if (r != -ENODEV) + FAILF("%s on a departed device rc=%ld, want ENODEV", what[i], r); + else + PASS(); + int rev = poll_fd_once(f, POLLIN, 0); + TEST("and the answer stamped the fd that asked"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), + "%s: revents=0x%x", what[i], rev); + close(f); + } + fd = prev; +} + +/* After t_disconnect, so the device is already gone: a watch armed now is armed + * against a service that has already terminated. + */ +static void t_watch_after_terminate(void) +{ + enter("t_watch_after_terminate"); + int prev = fd; + int fd3 = open(NODE, O_RDWR); + if (fd3 < 0) { + TEST("a fresh fd on the terminated loopback node"); + FAILF("open rc=%d", errno); + return; + } + fd = fd3; + + /* SUBMITURB arms the disconnect watch and then fails on its own argument: + * the setup packet is readable and the data it describes is not, so the URB + * gets as far as the ep0 event source -- which is where the watch is armed + * -- and copies no data in, reaching no IOKit entry point that could report + * the device gone. Nothing else on this fd can have learned it either, + * which leaves the watch as the only source of the wake, and a watch + * registered after the terminate had already fired was recorded and never + * called. + * + * A vendor request on the default control pipe is what gets there with + * nothing claimed: it is the one URB shape that skips the endpoint lookup + * (devio.c:1651) and, being vendor, skips check_ctrlrecip's implicit claim + * as well. Both of those now answer -ENODEV on a departed device, which is + * correct and is why the URB cannot be a bulk one on a claimed endpoint any + * more. + */ + long pgsz = sysconf(_SC_PAGESIZE); + uint8_t *pg = mmap(NULL, (size_t) pgsz * 2, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (pg == MAP_FAILED || munmap(pg + pgsz, (size_t) pgsz) != 0) { + TEST("a mapped setup packet followed by an unmapped page"); + FAILF("mmap rc=%d", errno); + close(fd3); + fd = prev; + return; + } + uint8_t *setup = pg + pgsz - 8; + setup[0] = 0x40; /* vendor, host-to-device: no implicit claim */ + setup[1] = 0x01; + setup[2] = setup[3] = setup[4] = setup[5] = 0; + setup[6] = 16; /* wLength; the data begins at the unmapped page */ + setup[7] = 0; + + struct urb u; + memset(&u, 0, sizeof(u)); + u.type = URB_TYPE_CONTROL; + u.endpoint = 0; + u.buffer = setup; + u.buffer_length = 8 + 16; + long r = submit(&u); + TEST("SUBMITURB with an unmapped buffer is -EFAULT after arming the watch"); + CHECK(r == -EFAULT, "submit rc=%ld", r); + + /* -EFAULT rather than -ENODEV is what keeps the assertion below about the + * watch: usbdev_ioctl's gate answers -ENODEV before the handler runs, so an + * fd already carrying the mark could not have reached the copy-in at all. + * The wake therefore has no other source than the registration this submit + * made. + */ + int rev = poll_once(POLLOUT | POLLWRNORM, 500); + TEST("the already-fired terminate reaches a watch registered afterwards"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + rev); + + munmap(pg, (size_t) pgsz); + close(fd3); + fd = prev; +} + +/* SETCONFIGURATION asks the device about its interfaces too, and is the one + * that did not go through usbdev_iface_service: it opened its own + * CreateInterfaceIterator to look for a bound host driver and discarded the + * IOReturn, so a departed device was read as "the enumeration ran and no driver + * is bound" and the op carried on. What it carried on into is not an error at + * all -- the config change reached the wire and was answered -- so + * SETCONFIGURATION returned 0 on a device that was gone and stamped nothing, + * leaving every later ioctl on the fd answering from an undisconnected fd where + * Linux's connected() gate answers -ENODEV to all of it. + * + * proc_setconfig refuses outright while any interface of the device is claimed, + * by this fd or another, so the enumeration is only reachable with the node's + * claims dropped. The main fd is stamped by now and its RELEASEINTERFACE would + * answer -ENODEV without releasing anything, so closing it is what drops its + * claim. Runs last for that reason. + */ +static void t_setconfig_on_a_departed_device(void) +{ + enter("t_setconfig_on_a_departed_device"); + close(fd); + fd = open(NODE, O_RDWR); + if (fd < 0) { + TEST("a fresh fd on the terminated loopback node"); + FAILF("open rc=%d", errno); + return; + } + unsigned cfg = 1; + long r = io(USBDEVFS_SETCONFIGURATION, &cfg); + TEST("SETCONFIGURATION on a departed device answers ENODEV"); + CHECK(r == -ENODEV, "SETCONFIGURATION rc=%ld, want ENODEV", r); + + /* Originated, not merely returned: the stamp is what the ops after it + * answer from, and its absence was half of this defect. + */ + int rev = poll_fd_once(fd, POLLIN, 0); + TEST("and the answer stamped the fd that asked"); + CHECK((rev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + rev); +} + +/* A terminate delivered while the fd it names is closing, and the guest fd + * number handed straight to somebody else. + * + * usbdev_interest_cb re-validates its packed token against used and the slot + * generation, and the peer walk next door tests dead beside used; this one did + * not. usbdev_fd_cleanup sets dead before it tears the slot down, and the + * teardown drain is two seconds wide against a wedged transfer, so a terminate + * arriving inside it stamped a slot whose fd had already closed -- on the guest + * fd number, which a sibling can hold by then. The stamp outlives the slot, + * since teardown clears disconnected afterwards and the cleanup's map clear is + * skipped precisely because a live entry now answers to that number. Measured + * 5/5: the sibling opened a DIFFERENT node, got the same guest fd, and was + * permanently poll revents=0x18 with every ioctl -ENODEV; without the terminate + * the same sibling is 0x0000. + * + * The same callback carries the peer walk, and only the stamp may be skipped. A + * second fd open on the terminated node that never submits an async URB arms no + * watch of its own, so the walk from the closing fd is the third of the three + * stamp sources tests/usbdev-ioctl-departed.tbl's REAPURB row names -- "or by a + * peer that learned first" -- and the only one such an fd has here. Guarding + * the walk with dead as well left it polling 0x0000 for a device that was gone, + * which is what the peer arms below measure. + * + * Its own process because it terminates the fixture device, which every + * scenario in the main run still needs. + */ +static int race_sibling_fd = -1; + +static void *race_open_sibling(void *arg) +{ + struct timespec ts = {0, (long) (intptr_t) arg * 1000 * 1000}; + nanosleep(&ts, NULL); + race_sibling_fd = open(OTHER_NODE, O_RDWR); + return NULL; +} + +static int t_terminate_during_close(void) +{ + enter("t_terminate_during_close"); + printf("a terminate delivered while the fd it names is closing\n"); + fd = open(NODE, O_RDWR); + if (fd < 0) { + TEST("the loopback node"); + FAILF("open rc=%d", errno); + return 1; + } + unsigned ifn = IFNUM; + io(USBDEVFS_CLAIMINTERFACE, &ifn); + + /* The sync-only peer, opened before the terminate and left alone: no claim, + * no submit, so no watch of its own. It takes a higher fd number than the + * one the sibling below races for. + */ + int peer = open(NODE, O_RDWR); + if (peer < 0) { + TEST("a second fd on the same loopback node"); + FAILF("open rc=%d", errno); + return 1; + } + fx_reset(); + fx_script("ep02:wedge(2500)"); + uint8_t out[8] = {0}; + struct urb u; + mk_bulk(&u, EP_OUT, out, sizeof(out)); + long sub = submit(&u); + TEST("a transfer whose abort outlives the teardown drain"); + CHECK(sub == 0, "SUBMITURB rc=%ld", sub); + + /* Inside the close's two-second drain from both ends: the terminate at 60 + * ms, the sibling's open at 20 ms so that the fd number is taken before + * usbdev_fd_cleanup decides whether to clear the map. + */ + TEST("a terminate armed to land inside that drain"); + CHECK(fx_terminate(60) == 0, "rc=%d", errno); + pthread_t th; + if (pthread_create(&th, NULL, race_open_sibling, (void *) (intptr_t) 20)) { + TEST("the sibling thread"); + FAILF("pthread_create failed"); + return 1; + } + double t0 = now_ms(); + close(fd); + double dt = now_ms() - t0; + pthread_join(th, NULL); + printf(" close returned after %.0f ms\n", dt); + + if (race_sibling_fd < 0) { + TEST("the sibling's open of another node"); + FAILF("open rc=%d", errno); + return 1; + } + int rev = poll_fd_once(race_sibling_fd, POLLIN | POLLOUT | POLLWRNORM, 0); + TEST("the sibling holding the closed fd number is not stamped"); + CHECK((rev & (POLLERR | POLLHUP)) == 0, "revents=0x%x", rev); + uint32_t caps = 0; + int ic = ioctl(race_sibling_fd, USBDEVFS_GET_CAPABILITIES, &caps); + TEST("and its ioctls do not answer for a device that was never its own"); + CHECK(!(ic < 0 && errno == ENODEV), "GET_CAPABILITIES rc=%d errno=%d", ic, + ic < 0 ? errno : 0); + close(race_sibling_fd); + + /* The peer's only route to the news is the walk the closing fd ran. Both + * arms answer from the same stamp: the revents remap, and the reap that + * ends on it rather than on -EAGAIN. + */ + int prev = poll_fd_once(peer, POLLIN | POLLOUT | POLLWRNORM, 0); + TEST("a sync-only peer on the terminated node learns the device is gone"); + CHECK((prev & (POLLERR | POLLHUP)) == (POLLERR | POLLHUP), "revents=0x%x", + prev); + void *reaped = NULL; + int pr = ioctl(peer, USBDEVFS_REAPURBNDELAY, &reaped); + int pe = pr < 0 ? errno : 0; + TEST("and its non-blocking reap answers ENODEV, not EAGAIN"); + CHECK(pr < 0 && pe == ENODEV, "rc=%d errno=%d", pr, pe); + close(peer); + + settle(2600); /* the wedge's late abort, before the process exits */ + SUMMARY("test-usbdev-urb-loopback terminate-race"); + return fails ? 1 : 0; +} + +int main(int argc, char **argv) +{ + setvbuf(stdout, NULL, _IOLBF, 0); + signal(SIGALRM, on_alarm); + if (argc > 1 && !strcmp(argv[1], "terminate-race")) + return t_terminate_during_close(); + enter("open"); + printf("usbdevfs async URB engine over the IOKit loopback fixture\n"); + fd = open(NODE, O_RDWR); + if (fd < 0) { + printf( + " cannot open %s (errno %d); this lane needs " + "ELFUSE_USB_FIXTURE=loopback\n", + NODE, errno); + return 1; + } + unsigned ifn = IFNUM; + TEST("CLAIMINTERFACE on the loopback interface"); + CHECK(io(USBDEVFS_CLAIMINTERFACE, &ifn) == 0, "claim rc=%d", errno); + uint32_t caps = 0; + TEST("GET_CAPABILITIES still names ZERO_PACKET and REAP_AFTER_DISCONNECT"); + CHECK(io(USBDEVFS_GET_CAPABILITIES, &caps) == 0 && caps == 0x11u, + "caps=0x%x", caps); + + t_complete_with_data(); + t_short(); + t_error_status(); + t_queue_order(); + t_discard_inflight(); + t_discard_queued(); + t_poll(); + t_reap_modes(); + t_zero_packet(); + t_fixture_contract(); + t_ready_level(); + t_drain_deadline(); + t_orphan_refund(); + t_control_charge(); + t_drain_timeout_restarts_fifos(); + t_follower_device_gone(); + t_disc_drained_reset(); + t_reap_ndelay_never_waits(); + t_enodev_never_precedes_the_urbs(); + t_ndelay_reaches_enodev(); + t_pipe_map_device_gone(); + t_discard_when_the_abort_is_refused(); + t_ep0_orphan_pins_the_device(); + t_disconnect(); + t_sync_op_originates_disconnect(); + t_iface_query_on_a_departed_device(); + t_iface_query_answers_for_the_device_not_the_claim(); + t_out_of_range_ifnum_on_a_departed_device(); + t_ops_that_reach_the_wire_ask_the_device_first(); + t_watch_after_terminate(); + t_setconfig_on_a_departed_device(); + + close(fd); + SUMMARY("test-usbdev-urb-loopback"); + return fails ? 1 : 0; +} diff --git a/tests/usbdev-ioctl-departed.tbl b/tests/usbdev-ioctl-departed.tbl new file mode 100644 index 00000000..b96178e1 --- /dev/null +++ b/tests/usbdev-ioctl-departed.tbl @@ -0,0 +1,184 @@ +# What Linux answers for the usbdevfs ioctls this layer names, issued on a +# device that has gone. +# +# Copyright 2026 elfuse contributors +# SPDX-License-Identifier: Apache-2.0 +# +# Read once out of drivers/usb/core/devio.c at tag v6.15 and recorded here, so +# that no comment in this tree has to assert it in prose. Every line number +# below is that file at that tag -- git blob +# f6ce6e26e0d45f67704f40b27c5a09cde9111259, which v6.15 through v6.19 all +# carry unchanged. Two facts decide every row: +# +# usbdev_do_ioctl (devio.c:2600-2641) lets REAPURB and REAPURBNDELAY past +# first and then refuses everything else with -ENODEV once connected(ps) is +# false, so the argument checks inside every proc_* handler live behind that +# gate and cannot be reached on a departed device. +# +# reap_as (devio.c:2081-2102) leaves its wait as soon as connected(ps) is +# false, and proc_reapurbnonblock (devio.c:2121-2135) answers -ENODEV rather +# than -EAGAIN in the same state, so the two ioctls that are let past the gate +# answer -ENODEV of their own accord once nothing is left to hand back. +# +# usbdev_poll (devio.c:2833-2847) raises EPOLLHUP from the same connected(ps) +# and EPOLLERR from the empty ps->list that usbdev_remove leaves behind, so +# every fd open on the node reads POLLERR|POLLHUP afterwards, whether or not +# it was the fd that asked. +# +# tests/test-usbdev-ioctl-departed.c drives each row and asserts the tuple. +# scripts/gen-usbdev-ioctl-departed.py joins this file against the dispatch in +# src/syscall/usbdev.c and emits tests/usbdev-ioctl-departed-vectors.h; a +# usbdevfs ioctl the layer implements with no row here fails the gate, so the +# table cannot fall behind the surface. +# +# The surface is not only what usbdev_ioctl dispatches. A row may name a +# USBDEVFS_ request that file defines and dispatches nowhere, and that row +# drives the default arm; the generator refuses to emit while usbdev_ioctl has +# a default arm and no row reaches it. Without those rows the join could not +# reach that arm at all, and the -ENOTTY it answered on a departed device sat +# under a comment claiming this table said what every arm answers. +# +# Columns: +# id row name; the lane defines departed_drive_ for it +# ioctl the USBDEVFS_ request the row drives +# phase fresh a freshly opened fd, nothing claimed on it +# held-claim an fd holding a claim taken before the device left +# held-release the same, for the release side +# kernel rc/errno/stamp/peer that Linux answers +# rc the ioctl(2) return +# errno errno when rc is -1, otherwise NONE +# stamp poll revents on the fd that asked +# peer poll revents on another fd open on the same node +# elfuse the same tuple where this layer deliberately answers otherwise, +# or - where it must match Linux +# cite the devio.c line the Linux answer was read from +# An indented line after a row is that row's note, and is printed with it. + +CONTROL CONTROL fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + DeviceRequestTO reaches the wire and IOKit answers the device gone. + +BULK BULK fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + The endpoint lookup claims the owning interface first, and the claim is + what asks. + +RESETEP RESETEP fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + ClearPipeStallBothEnds behind the same implicit claim. + +CLEAR_HALT CLEAR_HALT fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + As RESETEP. + +SETINTERFACE SETINTERFACE fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + The explicit claim ahead of SetAlternateInterface asks. + +SETCONFIGURATION SETCONFIGURATION fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + The bound-driver enumeration ahead of SetConfiguration asks. + +GETDRIVER GETDRIVER fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + The interface enumeration is the question. + +DISCONNECT_CLAIM DISCONNECT_CLAIM fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + As GETDRIVER. + +DRIVER_IOCTL IOCTL fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + USBDEVFS_IOCTL with the DISCONNECT sub-code; proc_ioctl carries a + connected() test of its own at devio.c:2330 behind the outer gate. + +RESET RESET fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + RESET touches no pipe with nothing claimed, so the question is asked + before the emulation runs at all. + +CLAIMINTERFACE CLAIMINTERFACE fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + An unheld interface number: the claim asks before it is granted. + +CLAIMINTERFACE_BOUND CLAIMINTERFACE fresh -1/ENODEV/ERRHUP/ERRHUP -1/EINVAL/NONE/NONE 2638 + An interface number past what this layer can represent. claimintf's own + bound (devio.c:788-789) sits behind the gate, so on Linux the number is + never judged on a device that is gone. Here the bound stays ahead of the + device question: it is arithmetic against the width of ps->ifclaimed and + reads no device state, and tests/test-usbdev-ioctl.c pins that ordering + on a node with no IOKit object behind it. Closing the gap would put a + registry enumeration ahead of a check that needs none. + +CLAIMINTERFACE_HELD CLAIMINTERFACE held-claim -1/ENODEV/ERRHUP/ERRHUP - 2638 + An interface this fd already holds. claimintf's already-claimed + short-circuit (devio.c:790-791) is behind the gate too. + +RELEASEINTERFACE RELEASEINTERFACE fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + An interface this fd does not hold: releaseintf's -EINVAL + (devio.c:815-833) is behind the gate. + +RELEASEINTERFACE_BOUND RELEASEINTERFACE fresh -1/ENODEV/ERRHUP/ERRHUP -1/EINVAL/NONE/NONE 2638 + The same bound on the release side, and the same reason for keeping it + ahead of the device question. + +RELEASEINTERFACE_HELD RELEASEINTERFACE held-release -1/ENODEV/ERRHUP/ERRHUP - 2638 + An interface this fd does hold, which is the arm that drops the claim + and closes the handle. + +SUBMITURB SUBMITURB fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + A well-formed bulk URB: the endpoint lookup's implicit claim asks. + +SUBMITURB_BADFLAGS SUBMITURB fresh -1/ENODEV/ERRHUP/ERRHUP -1/EINVAL/NONE/NONE 2638 + A URB carrying a flag bit no type accepts. proc_do_submiturb's argument + gate (devio.c:1644-1650) is behind the connected() gate on Linux and + ahead of the device question here, for the reason CLAIMINTERFACE_BOUND + gives: the flags mask, the transfer bound and the null buffer are + decided against the request alone, and tests/test-usbdev-ioctl.c asserts + the whole of that order against a node with no device behind it. + +DISCARDURB DISCARDURB fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + A userurb pointer this fd never submitted. proc_unlinkurb's own -EINVAL + is behind the gate, and unlike the two argument gates above it this one + reaches the guest: libusb_cancel_transfer passes the errno through, so + an unplug arrived as LIBUSB_ERROR_NOT_FOUND rather than NO_DEVICE. The + discard that finds its record asks the device through AbortPipe anyway, + so only this arm had to be given the question. + +REAPURB REAPURB fresh -1/ENODEV/ERRHUP/ERRHUP -1/EINTR/NONE/NONE 2104 + Let past the gate, and answered by reap_as: the wait ends on + connected(ps) going false, so a blocking reap on a departed device + returns rather than parking. Here the wait ends on this fd's own + disconnect stamp, which is raised by an IOKit answer, by the terminate + watch the async paths arm, or by a peer that learned first. A + synchronous-only fd has none of the three, so the reap waits; the row + measures that by interrupting it with a signal, which is the answer + proc_reapurb gives for a signal too (devio.c:2116-2117). + +REAPURBNDELAY REAPURBNDELAY fresh -1/ENODEV/ERRHUP/ERRHUP -1/EAGAIN/NONE/NONE 2132 + The same gap without the wait: proc_reapurbnonblock picks between + -EAGAIN and -ENODEV on connected(ps), and this layer picks on the fd's + own stamp. + +GET_CAPABILITIES GET_CAPABILITIES fresh -1/ENODEV/ERRHUP/ERRHUP 0/NONE/NONE/NONE 2638 + Answered from a build constant, with no wire behind it, the way stat + and the descriptor read on the same node are. Making it ask would put a + registry enumeration on a call that needs none. + +GET_SPEED GET_SPEED fresh -1/ENODEV/ERRHUP/ERRHUP 2/NONE/NONE/NONE 2638 + Answered from the speed recorded at open, as GET_CAPABILITIES. The rc + is the USB_SPEED_FULL enumerator the loopback device models. + +CONNECTINFO CONNECTINFO fresh -1/ENODEV/ERRHUP/ERRHUP 0/NONE/NONE/NONE 2638 + Answered from the devnum and speed recorded at open, as GET_SPEED. + +DISCSIGNAL DISCSIGNAL fresh -1/ENODEV/ERRHUP/ERRHUP 0/NONE/NONE/NONE 2638 + Stores a signal number this layer never delivers, so there is nothing + for it to ask the device about. + +HUB_PORTINFO HUB_PORTINFO fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + A request this layer defines and dispatches nowhere, so it reaches + usbdev_ioctl's default arm. Linux implements it (proc_gethubportinfo) + and still answers -ENODEV, because connected() is decided before the + switch that would reach it; what the arm answers on a device that is + still here, -ENOTTY, is outside this table. + +ALLOC_STREAMS ALLOC_STREAMS fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + The default arm again, and the row with a guest behind it: libusb maps + -ENOTTY on this request to LIBUSB_ERROR_NOT_SUPPORTED and -ENODEV to + LIBUSB_ERROR_NO_DEVICE, so an unplugged device that answered -ENOTTY + read as a kernel with no bulk-streams support and stayed that way. + +WAIT_FOR_RESUME WAIT_FOR_RESUME fresh -1/ENODEV/ERRHUP/ERRHUP - 2638 + The default arm reached by a request carrying no argument at all, + which is the shape that would have slipped past an arm asking the + device only when it had a buffer to read.