Files
usb-server/driver/windows/queue.c
duffyduckandClaude Opus 5 9ed473a965 Fix HID transfers, harden the tunnel, add E2E crypto and direct peers
The HID failure came down to the endpoint type map being indexed by
endpoint number without the direction bit. A composite device can have
endpoint 1 as both interrupt IN (0x81) and bulk OUT (0x01); the last one
read won, so interrupt URBs were submitted as bulk and the kernel rejected
them. The device attached and stayed silent.

Endpoint data now comes from the raw descriptors read from /dev/bus/usb
rather than sysfs, which only ever exposes the active alternate setting —
a webcam's isochronous endpoints are invisible there because they only
exist after SET_INTERFACE. Two sysfs parsing bugs fell out of that too:
the numeric endpoint attributes are hex without a prefix (wMaxPacketSize
"0040" was read as 40, not 64), and bInterval was never read at all.

Reliability: three places could freeze the whole process. The share path
fed io.Pipe from the WebSocket read loop, so one slow USB transfer stalled
every tunnel and the keepalives with them. The relay wrote to client
sockets while holding the hub lock, so one peer that stopped reading
blocked routing and registration for everyone. Control transfers ran
inline in the protocol loop behind a 5s timeout. Also fixed: a use-after-
free where a discarded URB's memory could be collected while the kernel
still owned it, a reap loop that spun at 100% CPU on ioctl errors, a
missing attach timeout, a double close(done) panic, and Hash[:8] in the
relay's log line, which let a client with a short hash take the server
down.

Adds mode "both", so one client can offer and consume devices at once.
The tunnel and client-left callbacks became multicast for it: as plain
fields the second manager to register silently unhooked the first.

Tunnel traffic is now AES-256-GCM end to end, on the relay path as well
as directly. The key is derived from the three tokens, not from the group
hash — the relay is told the hash, so a key derived from it would protect
nothing from the one party in the middle. Group IDs are unchanged, so
existing setups keep working; only clients configured without the tokens
drop to unencrypted, relay-only operation.

Peers now try to connect directly, with the relay supplying the public
address neither side can determine for itself. Candidates are raced
because an unreachable address hangs until timeout rather than refusing.
Falling back to the relay is not an error.

Platform reach: cross-compiled targets for ARM, MIPS and RISC-V (the
Linux client needed no code changes — usbdevfs is not architecture
specific), multi-arch Docker images, an Android bridge that accepts
devices over SCM_RIGHTS because apps cannot open /dev/bus/usb, and macOS
builds via system_profiler enumeration.

Adds a Windows KMDF filter driver under driver/windows with its Go side.
UNTESTED: it has never been compiled or run, needs the WDK to build and
an EV certificate to distribute. Treat it as a starting point.

Adds "usb-client diag": says per machine whether sharing and using are
possible, what stands in the way, and what fixes it. Reports can be
uploaded to a relay to get them off machines that are awkward to copy
from.

96 tests, all green under -race. Builds for linux, windows and darwin on
amd64 and arm64.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:02:04 +02:00

556 lines
18 KiB
C

/*
* usbshare - IOCTL handling and URB forwarding
*/
#include "usbshare.h"
static VOID UsbShareCompleteTransfer(
_In_ WDFREQUEST Request,
_In_ WDFIOTARGET Target,
_In_ PWDF_REQUEST_COMPLETION_PARAMS Params,
_In_ WDFCONTEXT Context);
static NTSTATUS UsbShareHandleClaim(_In_ PDEVICE_CONTEXT Context, _In_ WDFREQUEST Request);
static NTSTATUS UsbShareHandleGetDescriptors(_In_ PDEVICE_CONTEXT Context, _In_ WDFREQUEST Request);
static NTSTATUS UsbShareHandleSubmit(_In_ PDEVICE_CONTEXT Context, _In_ WDFREQUEST Request);
static NTSTATUS UsbShareHandleCancel(_In_ PDEVICE_CONTEXT Context, _In_ WDFREQUEST Request);
static NTSTATUS UsbShareHandleSetInterface(_In_ PDEVICE_CONTEXT Context, _In_ WDFREQUEST Request);
static NTSTATUS UsbShareHandleClearHalt(_In_ PDEVICE_CONTEXT Context, _In_ WDFREQUEST Request);
VOID
UsbShareEvtIoDeviceControl(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ size_t OutputBufferLength,
_In_ size_t InputBufferLength,
_In_ ULONG IoControlCode
)
{
WDFDEVICE device = WdfIoQueueGetDevice(Queue);
PDEVICE_CONTEXT context = GetDeviceContext(device);
NTSTATUS status;
UNREFERENCED_PARAMETER(OutputBufferLength);
UNREFERENCED_PARAMETER(InputBufferLength);
switch (IoControlCode) {
case IOCTL_USBSHARE_CLAIM:
status = UsbShareHandleClaim(context, Request);
break;
case IOCTL_USBSHARE_RELEASE:
UsbShareReleaseClaim(context, WdfRequestGetFileObject(Request));
status = STATUS_SUCCESS;
break;
case IOCTL_USBSHARE_GET_DESCRIPTORS:
status = UsbShareHandleGetDescriptors(context, Request);
break;
case IOCTL_USBSHARE_SUBMIT:
status = UsbShareHandleSubmit(context, Request);
/*
* A submitted transfer completes asynchronously; the completion
* routine owns the request from here.
*/
if (status == STATUS_PENDING) {
return;
}
break;
case IOCTL_USBSHARE_CANCEL:
status = UsbShareHandleCancel(context, Request);
break;
case IOCTL_USBSHARE_SET_INTERFACE:
status = UsbShareHandleSetInterface(context, Request);
break;
case IOCTL_USBSHARE_CLEAR_HALT:
status = UsbShareHandleClearHalt(context, Request);
break;
case IOCTL_USBSHARE_RESET:
status = WdfUsbTargetDeviceResetPortSynchronously(context->UsbDevice);
break;
default:
/*
* Not ours. As a filter we must pass it on rather than fail it —
* some other component in the stack may be waiting for the answer.
*/
{
WDF_REQUEST_SEND_OPTIONS options;
WDF_REQUEST_SEND_OPTIONS_INIT(&options, WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET);
WdfRequestFormatRequestUsingCurrentType(Request);
if (!WdfRequestSend(Request, WdfDeviceGetIoTarget(device), &options)) {
WdfRequestComplete(Request, WdfRequestGetStatus(Request));
}
return;
}
}
WdfRequestComplete(Request, status);
}
static NTSTATUS
UsbShareHandleClaim(
_In_ PDEVICE_CONTEXT Context,
_In_ WDFREQUEST Request
)
{
NTSTATUS status;
PUSBSHARE_DEVICE_INFO info;
WDFFILEOBJECT fileObject = WdfRequestGetFileObject(Request);
status = WdfRequestRetrieveOutputBuffer(Request, sizeof(USBSHARE_DEVICE_INFO),
(PVOID *)&info, NULL);
if (!NT_SUCCESS(status)) {
return status;
}
WdfSpinLockAcquire(Context->ClaimLock);
if (Context->Claimed && Context->ClaimOwner != fileObject) {
WdfSpinLockRelease(Context->ClaimLock);
return STATUS_DEVICE_BUSY;
}
Context->Claimed = TRUE;
Context->ClaimOwner = fileObject;
WdfSpinLockRelease(Context->ClaimLock);
*info = Context->Info;
WdfRequestSetInformation(Request, sizeof(USBSHARE_DEVICE_INFO));
KdPrint(("usbshare: device claimed (%04x:%04x)\n", info->VendorId, info->ProductId));
return STATUS_SUCCESS;
}
static NTSTATUS
UsbShareHandleGetDescriptors(
_In_ PDEVICE_CONTEXT Context,
_In_ WDFREQUEST Request
)
{
NTSTATUS status;
PVOID buffer;
size_t bufferLength;
if (Context->Descriptors == NULL) {
status = UsbShareBuildDescriptorBlob(Context);
if (!NT_SUCCESS(status)) {
return status;
}
}
status = WdfRequestRetrieveOutputBuffer(Request, 1, &buffer, &bufferLength);
if (!NT_SUCCESS(status)) {
return status;
}
if (bufferLength < Context->DescriptorsLength) {
/* Report the needed size so the caller can retry. */
WdfRequestSetInformation(Request, Context->DescriptorsLength);
return STATUS_BUFFER_TOO_SMALL;
}
RtlCopyMemory(buffer, Context->Descriptors, Context->DescriptorsLength);
WdfRequestSetInformation(Request, Context->DescriptorsLength);
return STATUS_SUCCESS;
}
/*
* Builds the descriptor blob: device descriptor followed by every
* configuration descriptor, matching what Linux returns when reading a
* usbdevfs file. The client parses both with the same code.
*/
NTSTATUS
UsbShareBuildDescriptorBlob(
_In_ PDEVICE_CONTEXT Context
)
{
NTSTATUS status;
USB_DEVICE_DESCRIPTOR deviceDescriptor;
PUCHAR blob = NULL;
ULONG blobSize = 0;
ULONG offset;
UCHAR configIndex;
WdfUsbTargetDeviceGetDeviceDescriptor(Context->UsbDevice, &deviceDescriptor);
/* First pass: total up the sizes. */
blobSize = sizeof(USB_DEVICE_DESCRIPTOR);
for (configIndex = 0; configIndex < deviceDescriptor.bNumConfigurations; configIndex++) {
USHORT configSize = 0;
status = WdfUsbTargetDeviceRetrieveConfigDescriptor(Context->UsbDevice, NULL, &configSize);
if (status != STATUS_BUFFER_TOO_SMALL && !NT_SUCCESS(status)) {
return status;
}
blobSize += configSize;
/*
* Only configuration 0 can be retrieved through this API; devices
* with several configurations would need a raw control transfer per
* configuration. They are rare enough to leave for later.
*/
break;
}
blob = (PUCHAR)ExAllocatePool2(POOL_FLAG_NON_PAGED, blobSize, USBSHARE_POOL_TAG);
if (blob == NULL) {
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlCopyMemory(blob, &deviceDescriptor, sizeof(USB_DEVICE_DESCRIPTOR));
offset = sizeof(USB_DEVICE_DESCRIPTOR);
{
USHORT configSize = (USHORT)(blobSize - offset);
status = WdfUsbTargetDeviceRetrieveConfigDescriptor(Context->UsbDevice,
blob + offset,
&configSize);
if (!NT_SUCCESS(status)) {
ExFreePoolWithTag(blob, USBSHARE_POOL_TAG);
return status;
}
}
Context->Descriptors = blob;
Context->DescriptorsLength = blobSize;
return STATUS_SUCCESS;
}
static NTSTATUS
UsbShareHandleSubmit(
_In_ PDEVICE_CONTEXT Context,
_In_ WDFREQUEST Request
)
{
NTSTATUS status;
PUSBSHARE_TRANSFER transfer;
size_t inputLength;
PUCHAR payload;
WDFMEMORY urbMemory;
PURB urb;
WDFUSBPIPE pipe;
PREQUEST_CONTEXT reqContext;
WDF_OBJECT_ATTRIBUTES attributes;
WDFIOTARGET target;
if (!UsbShareIsClaimed(Context)) {
return STATUS_INVALID_DEVICE_STATE;
}
status = WdfRequestRetrieveInputBuffer(Request, sizeof(USBSHARE_TRANSFER),
(PVOID *)&transfer, &inputLength);
if (!NT_SUCCESS(status)) {
return status;
}
if (inputLength < sizeof(USBSHARE_TRANSFER) + transfer->BufferLength) {
return STATUS_BUFFER_TOO_SMALL;
}
payload = (PUCHAR)transfer + sizeof(USBSHARE_TRANSFER);
/* Attach a context so a later cancel can find this request. */
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, REQUEST_CONTEXT);
status = WdfObjectAllocateContext(Request, &attributes, (PVOID *)&reqContext);
if (!NT_SUCCESS(status)) {
return status;
}
reqContext->TransferId = transfer->Id;
reqContext->ExpectedLength = transfer->BufferLength;
WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
attributes.ParentObject = Request;
if (transfer->Type == USBSHARE_TRANSFER_CONTROL) {
status = WdfUsbTargetDeviceCreateUrb(Context->UsbDevice, &attributes,
&urbMemory, &urb);
if (!NT_SUCCESS(status)) {
return status;
}
/*
* The setup packet arrives in USB wire order and is copied verbatim.
* Reinterpreting the fields here would only introduce a chance to get
* the endianness wrong.
*/
UsbBuildVendorRequest(urb,
URB_FUNCTION_VENDOR_DEVICE,
sizeof(struct _URB_CONTROL_VENDOR_OR_CLASS_REQUEST),
(transfer->Direction == USBSHARE_DIR_IN)
? USBD_TRANSFER_DIRECTION_IN : 0,
0,
transfer->Setup[0], /* bmRequestType */
transfer->Setup[1], /* bRequest */
*(USHORT *)&transfer->Setup[2], /* wValue */
*(USHORT *)&transfer->Setup[4], /* wIndex */
payload,
NULL,
transfer->BufferLength,
NULL);
} else {
pipe = Context->Pipes[transfer->EndpointAddress];
if (pipe == NULL) {
return STATUS_INVALID_PARAMETER;
}
status = WdfUsbTargetDeviceCreateUrb(Context->UsbDevice, &attributes,
&urbMemory, &urb);
if (!NT_SUCCESS(status)) {
return status;
}
/*
* Bulk and interrupt share one URB function; the pipe handle decides
* which it actually is.
*/
urb->UrbBulkOrInterruptTransfer.Hdr.Length =
sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER);
urb->UrbBulkOrInterruptTransfer.Hdr.Function =
URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER;
urb->UrbBulkOrInterruptTransfer.PipeHandle = WdfUsbTargetPipeWdmGetPipeHandle(pipe);
urb->UrbBulkOrInterruptTransfer.TransferBuffer = payload;
urb->UrbBulkOrInterruptTransfer.TransferBufferLength = transfer->BufferLength;
urb->UrbBulkOrInterruptTransfer.TransferBufferMDL = NULL;
urb->UrbBulkOrInterruptTransfer.UrbLink = NULL;
urb->UrbBulkOrInterruptTransfer.TransferFlags =
(transfer->Direction == USBSHARE_DIR_IN)
? (USBD_TRANSFER_DIRECTION_IN | USBD_SHORT_TRANSFER_OK)
: 0;
}
reqContext->UrbMemory = urbMemory;
reqContext->Urb = urb;
target = WdfUsbTargetDeviceGetIoTarget(Context->UsbDevice);
status = WdfUsbTargetDeviceFormatRequestForUrb(Context->UsbDevice, Request,
urbMemory, NULL);
if (!NT_SUCCESS(status)) {
return status;
}
WdfRequestSetCompletionRoutine(Request, UsbShareCompleteTransfer, Context);
/* Track it so a cancel can find it. */
WdfSpinLockAcquire(Context->PendingLock);
WdfCollectionAdd(Context->PendingTransfers, Request);
WdfSpinLockRelease(Context->PendingLock);
if (!WdfRequestSend(Request, target, WDF_NO_SEND_OPTIONS)) {
status = WdfRequestGetStatus(Request);
WdfSpinLockAcquire(Context->PendingLock);
WdfCollectionRemove(Context->PendingTransfers, Request);
WdfSpinLockRelease(Context->PendingLock);
return status;
}
return STATUS_PENDING;
}
static VOID
UsbShareCompleteTransfer(
_In_ WDFREQUEST Request,
_In_ WDFIOTARGET Target,
_In_ PWDF_REQUEST_COMPLETION_PARAMS Params,
_In_ WDFCONTEXT CompletionContext
)
{
PDEVICE_CONTEXT context = (PDEVICE_CONTEXT)CompletionContext;
PREQUEST_CONTEXT reqContext = GetRequestContext(Request);
PUSBSHARE_TRANSFER_RESULT result;
NTSTATUS status;
size_t outputLength;
ULONG transferred = 0;
PUCHAR outPayload;
UNREFERENCED_PARAMETER(Target);
WdfSpinLockAcquire(context->PendingLock);
WdfCollectionRemove(context->PendingTransfers, Request);
WdfSpinLockRelease(context->PendingLock);
if (reqContext->Urb != NULL) {
transferred = reqContext->Urb->UrbBulkOrInterruptTransfer.TransferBufferLength;
}
status = WdfRequestRetrieveOutputBuffer(Request, sizeof(USBSHARE_TRANSFER_RESULT),
(PVOID *)&result, &outputLength);
if (!NT_SUCCESS(status)) {
WdfRequestComplete(Request, status);
return;
}
result->Id = reqContext->TransferId;
result->Status = Params->IoStatus.Status;
result->UsbdStatus = (reqContext->Urb != NULL)
? reqContext->Urb->UrbHeader.Status : 0;
result->ActualLength = transferred;
/*
* Copy the received payload after the result header, but only as much as
* the output buffer holds — a device may return more than expected.
*/
if (transferred > 0 && outputLength > sizeof(USBSHARE_TRANSFER_RESULT)) {
ULONG room = (ULONG)(outputLength - sizeof(USBSHARE_TRANSFER_RESULT));
ULONG copy = (transferred < room) ? transferred : room;
outPayload = (PUCHAR)result + sizeof(USBSHARE_TRANSFER_RESULT);
RtlCopyMemory(outPayload,
reqContext->Urb->UrbBulkOrInterruptTransfer.TransferBuffer,
copy);
WdfRequestSetInformation(Request, sizeof(USBSHARE_TRANSFER_RESULT) + copy);
} else {
WdfRequestSetInformation(Request, sizeof(USBSHARE_TRANSFER_RESULT));
}
/*
* Always complete successfully: the transfer's own outcome travels in the
* result structure. Failing the IOCTL would lose the distinction between
* "the ioctl did not work" and "the device stalled".
*/
WdfRequestComplete(Request, STATUS_SUCCESS);
}
static NTSTATUS
UsbShareHandleCancel(
_In_ PDEVICE_CONTEXT Context,
_In_ WDFREQUEST Request
)
{
NTSTATUS status;
PUSBSHARE_CANCEL cancel;
ULONG i, count;
WDFREQUEST target = NULL;
status = WdfRequestRetrieveInputBuffer(Request, sizeof(USBSHARE_CANCEL),
(PVOID *)&cancel, NULL);
if (!NT_SUCCESS(status)) {
return status;
}
WdfSpinLockAcquire(Context->PendingLock);
count = WdfCollectionGetCount(Context->PendingTransfers);
for (i = 0; i < count; i++) {
WDFREQUEST candidate = (WDFREQUEST)WdfCollectionGetItem(Context->PendingTransfers, i);
PREQUEST_CONTEXT candidateContext = GetRequestContext(candidate);
if (candidateContext != NULL && candidateContext->TransferId == cancel->Id) {
target = candidate;
break;
}
}
WdfSpinLockRelease(Context->PendingLock);
if (target == NULL) {
/* Already finished. Not an error: the caller gets its result anyway. */
return STATUS_SUCCESS;
}
WdfRequestCancelSentRequest(target);
return STATUS_SUCCESS;
}
static NTSTATUS
UsbShareHandleSetInterface(
_In_ PDEVICE_CONTEXT Context,
_In_ WDFREQUEST Request
)
{
NTSTATUS status;
PUSBSHARE_SET_INTERFACE params;
WDF_USB_INTERFACE_SELECT_SETTING_PARAMS settingParams;
if (!UsbShareIsClaimed(Context)) {
return STATUS_INVALID_DEVICE_STATE;
}
status = WdfRequestRetrieveInputBuffer(Request, sizeof(USBSHARE_SET_INTERFACE),
(PVOID *)&params, NULL);
if (!NT_SUCCESS(status)) {
return status;
}
if (Context->UsbInterface == NULL) {
return STATUS_INVALID_DEVICE_STATE;
}
/*
* Going through the framework rather than sending a raw SET_INTERFACE is
* essential: the USB stack has to re-open the pipes and, for isochronous
* endpoints, reserve bandwidth. A raw control transfer changes the device
* without telling the stack, after which every later transfer fails.
*/
WDF_USB_INTERFACE_SELECT_SETTING_PARAMS_INIT_SETTING(&settingParams,
params->AlternateSetting);
status = WdfUsbInterfaceSelectSetting(Context->UsbInterface,
WDF_NO_OBJECT_ATTRIBUTES,
&settingParams);
if (!NT_SUCCESS(status)) {
return status;
}
/* Pipe handles change with the setting, so rebuild the map. */
RtlZeroMemory(Context->Pipes, sizeof(Context->Pipes));
{
BYTE pipeCount = WdfUsbInterfaceGetNumConfiguredPipes(Context->UsbInterface);
BYTE i;
for (i = 0; i < pipeCount; i++) {
WDF_USB_PIPE_INFORMATION pipeInfo;
WDFUSBPIPE pipe;
WDF_USB_PIPE_INFORMATION_INIT(&pipeInfo);
pipe = WdfUsbInterfaceGetConfiguredPipe(Context->UsbInterface, i, &pipeInfo);
if (pipe != NULL) {
Context->Pipes[pipeInfo.EndpointAddress] = pipe;
WdfUsbTargetPipeSetNoMaximumPacketSizeCheck(pipe);
}
}
}
return STATUS_SUCCESS;
}
static NTSTATUS
UsbShareHandleClearHalt(
_In_ PDEVICE_CONTEXT Context,
_In_ WDFREQUEST Request
)
{
NTSTATUS status;
PUSBSHARE_CLEAR_HALT params;
WDFUSBPIPE pipe;
if (!UsbShareIsClaimed(Context)) {
return STATUS_INVALID_DEVICE_STATE;
}
status = WdfRequestRetrieveInputBuffer(Request, sizeof(USBSHARE_CLEAR_HALT),
(PVOID *)&params, NULL);
if (!NT_SUCCESS(status)) {
return status;
}
pipe = Context->Pipes[params->EndpointAddress];
if (pipe == NULL) {
return STATUS_INVALID_PARAMETER;
}
return WdfUsbTargetPipeResetSynchronously(pipe, WDF_NO_HANDLE, NULL);
}