Skip to contentSkip to conversation

hw/usb: Add a usbredir server transport

PATCH New Jamin Lin 1 participant 20 messages Nov 10

Subsystems (4)
lore
Share & tools
lore

Jamin Lin

author
#0 lore raw

v9 cover hw/usb: Add a usbredir server transport

This series adds a usbredir server transport to redirect a USB device emulated in one QEMU instance to a USB host controller emulated in another one over the usbredir protocol.

The v1 series included both the usbredir server transport and the ASPEED AST1030 USB Device Controller (UDC) support. Following review feedback, this series contains only the usbredir server transport. The ASPEED AST1030 UDC support will be sent separately.

This work is part of a larger plan to model USB device-side support on ASPEED BMC/BIC SoCs, which has three goals:

1. Model the ASPEED UDC (AST2600 / AST1030). The AST2600 also has USB host (EHCI) controllers, so that work targets the AST2600 UDC: its gadget can be attached to the SoC's own EHCI bus, letting the guest enumerate its own gadget and exercise the UDC end-to-end. [Done]
https://lore.kernel.org/qemu-devel/[email protected]/

2. AST1030 UDC. The AST1030 has no USB host controller, so testing its UDC needs a second QEMU instance. The UDC gadget is redirected out of the guest with libusbredir and attached to another QEMU that runs a USB host (a VMM, or an AST2600 / AST2700 guest). [this series]

3. ASPEED vHub, as a longer-term goal towards BMC KVM / Virtual Media support in QEMU. [future]

This series implements goal 2.

The transport
=============

x-usb-redir-server exports a locally emulated USB device to a remote USB host over the usbredir protocol, so a device emulated in one QEMU instance can be enumerated by a host controller emulated in another one.

The left column is a request going to the device. The right column is the answer coming back. The middle hop carries usbredir messages over a socket. The top and bottom hops carry USBPackets inside QEMU.

    remote QEMU: guest driver -> EHCI/XHCI
             |                     ^
   USBPacket |                     | USBPacket
             v                     |
    "usb-redir" (the client)
             |                     ^
    usbredir |   chardev socket    | usbredir
             v                     |
    x-usb-redir-server (the server, this series)
             |                     ^
   USBPacket |                     | USBPacket
             v                     |
    any USBDevice, "-device <dev>,bus=<id>.0"

"usb-redir" (hw/usb/redirect.c) is the client:
- it takes a USBPacket from the remote guest and writes it to the socket as a usbredir message
- it reads the answer from the socket and completes the USBPacket

x-usb-redir-server is the server. It does the same thing, but backwards:
- it reads a usbredir message from the socket and runs it as a USBPacket on the bus below
- it takes the result of that USBPacket and writes it back to the same socket as a usbredir message, for the client to read

A USB device has to sit on a USB bus, and in QEMU a USB bus is always made by a host controller. So x-usb-redir-server makes one and acts as the host controller on this side. It models no real chip: its cable is the chardev socket. The real host is in the other QEMU.

The type name has an "x-" prefix because the device is experimental. It has to sit on the sysbus so that "-device <dev>,bus=<id>.0" can find the USB bus it provides, and that may change later.

usbredir carries one device, not a bus. A hub cannot be exported: the protocol has no device address field. To export several devices, run one x-usb-redir-server per device, each with its own chardev.

Testing
=======

A plain usb-storage device was used to test the transport. The storage device is attached to x-usb-redir-server and exported over a Unix socket:

$ qemu-system-aarch64 ...
-chardev socket,id=usbredir0,path=/tmp/usbredir0.sock,server=on,wait=off
-device x-usb-redir-server,id=usbredir0,chardev=usbredir0
-drive id=usbdisk,if=none,file=image0.ext4,format=raw
-device usb-storage,bus=usbredir0.0,id=mystorage,drive=usbdisk

The exported device can then be attached to EHCI bus 3 of an AST2700 guest using the existing usb-redir client:

    -chardev socket,id=storage,path=/tmp/usbredir0.sock,reconnect-ms=1000 \
    -device usb-redir,chardev=storage,bus=usb-bus.3

The AST2700 Linux guest enumerates the redirected USB storage device:

root@ast2700-default:~# lsusb
unable to initialize usb spec
Bus 001 Device 001: ID 1d6b:0001 Linux 6.18.36-v00.08.03-gaf2f426c5786 uhci_hcd Generic UHCI Host Controller
Bus 002 Device 001: ID 1d6b:0002 Linux 6.18.36-v00.08.03-gaf2f426c5786 ehci_hcd EHCI Host Controller
Bus 002 Device 002: ID 46f4:0001 QEMU QEMU USB HARDDRIVE

This also demonstrates that x-usb-redir-server is not specific to the ASPEED UDC. Any USBDevice can be attached to its USB bus and exported to a host controller in another QEMU instance.

v1:
1. Add a usbredir server device
2. Announce the exported device
3. Implement control transfers
4. Implement bulk and interrupt transfers
5. Stream interrupt IN endpoints

v2:
1. Split the original series into USB and ASPEED parts as suggested during review.
2. Keep this series focused on the usbredir server transport.
3. Move the ASPEED UDC changes, AST1030 UDC support and its functional test to a separate series.

v3:
1. Move include/hw/usb/redirect-server.h into redirect-server.c.
2. Drop the announce timer. The firmware does not toggle the pull-up, and the delay would be machine specific anyway.
3. Rename the type to "x-usb-redir-server" to mark it experimental.

v4:
1. ep_info now says what the host chose, both the configuration and the alternate setting. Keep every configuration descriptor, not just the last one.
2. look at the request type too, not only the number
3. wait for the endpoint's bInterval before asking the device
again.

Patch 7 lost its Reviewed-by because it changed. It used to ask the device again from a bottom half, which runs as soon as the main loop comes back. So there was no wait at all.

A keyboard answers NAK while no key is down, and the retry timer paces that, so it looked fine. usb-net's status endpoint answers every time. The loop never stopped: ask, answer, ask again, about 17000 times a second. The same main loop runs the guest's timers, and it had no room left. The guest stopped feeding its watchdog, and the watchdog reset the machine.

bInterval is the rate the endpoint asks to be polled at, and the rate a real host controller uses. The stream now waits for it.

v5:
1. Use a separate timer and bottom half for each endpoint.

v6:
1. Fix a build failure from a missing header.
2. Free the packet data in the control, bulk and interrupt callbacks.
3. Cancel the packet before cleanup on USB_RET_REMOVE_FROM_QUEUE.
4. Send no reply when the cancelled transfer is not found.
5. End the interrupt stream only on USB_RET_STALL.
6. Keep a USBRedirServer pointer instead of pointer arithmetic.
7. Cancel the BH and delete the timer in stop_interrupt_receiving().
8. Start the interrupt stream loops at 1, endpoint 0 never streams.

v7:
1. Add a flag for the automatic hub and leave it off by default. UHCI, OHCI and dwc2 turn it on.
2. Drive a control transfer with one packet and the setup fields in pkt.parameter, instead of walking the token stages.
3. Keep a request the device answered NAK and send it again when the endpoint wakes, with a timer for the devices that never do.
4. Give each endpoint one struct, holding its waiting list, its retry timer and its descriptor fields.
5. Ask more slowly after a streamed request answered NAK or failed, and keep the endpoint's own rate for the good case.

v8:
1. Report how many bytes the device moved on a bulk or interrupt OUT transfer.
2. Implement isochronous transfers for an OUT endpoint.
3. Announce the device from a bottom half, and reset it first. The old path hit an assertion, and the device has to be in DEFAULT state before it can answer anything.
4. Avoid cancel a streamed interrupt or isochronous packet.
5. Turn the automatic hub off by default.
6. Give every USB host controller an auto-hub property, off by default.

v9:
1. Read the device and configuration descriptors before announcing the device. Send interface_info and ep_info with real values before device_connect. Close the connection if the read fails.
2. Add a TODO to the auto-hub patches. The compat entries move to hw_compat_11_2 after the 11.2 release.
3. Do not hit an assertion when a control packet arrives in the same read as hello.
4. Send cancelled for every packet that stop_transfers() drops, so the host does not wait forever.
5. docs: say 12.0 instead of 11.2, and explain how to get the hub back with "-device usb-hub" or "-global <controller>.auto-hub=on".

Jamin Lin (19):
hw/usb/bus: Add an auto-hub flag to the USB bus hw/usb/hcd-xhci: Add an auto-hub property and leave it off hw/usb/hcd-ehci: Add an auto-hub property and leave it off hw/usb/hcd-uhci: Add an auto-hub property and leave it off hw/usb/hcd-ohci: Add an auto-hub property and leave it off hw/usb/hcd-dwc2: Add an auto-hub property and leave it off hw/usb/xen-usb: Do not insert an automatic hub hw/arm/npcm7xx: Keep the automatic hub
hw/usb/bus: Turn off the automatic hub by default docs/system/devices/usb: Note the automatic hub change hw/usb/redirect-server: Add a usbredir server device hw/usb/redirect-server: Connect usbredirparser to a chardev hw/usb/redirect-server: Announce the exported device hw/usb/redirect-server: Implement control transfers hw/usb/redirect-server: Send device and interface information hw/usb/redirect-server: Implement bulk and interrupt transfers hw/usb/redirect-server: Stream interrupt IN endpoints hw/usb/redirect-server: Implement isochronous OUT transfers hw/arm/aspeed: Enable the usbredir server transport

 docs/system/devices/usb.rst  |   17 +-
 hw/usb/hcd-dwc2.h            |    1 +
 hw/usb/hcd-ehci.h            |    4 +-
 hw/usb/hcd-ohci.h            |    1 +
 hw/usb/hcd-uhci.h            |    1 +
 hw/usb/hcd-xhci.h            |    1 +
 include/hw/usb/usb.h         |    1 +
 hw/arm/aspeed.c              |    7 +
 hw/arm/bcm2835_peripherals.c |    1 +
 hw/arm/npcm7xx.c             |    2 +
 hw/core/machine.c            |    6 +
 hw/usb/bus.c                 |    3 +-
 hw/usb/hcd-dwc2.c            |    2 +
 hw/usb/hcd-ehci.c            |    1 +
 hw/usb/hcd-ohci-pci.c        |    1 +
 hw/usb/hcd-ohci-sysbus.c     |    1 +
 hw/usb/hcd-ohci.c            |    1 +
 hw/usb/hcd-uhci.c            |    3 +
 hw/usb/hcd-xhci.c            |    2 +
 hw/usb/pc87560-ohci.c        |    1 +
 hw/usb/redirect-server.c     | 2671 ++++++++++++++++++++++++++++++++++
 hw/usb/xen-usb.c             |    1 +
 hw/usb/meson.build           |    3 +-
 hw/usb/trace-events          |   46 +
 24 files changed, 2773 insertions(+), 5 deletions(-)
 create mode 100644 hw/usb/redirect-server.c
Signature · 1 line
2.43.0
author
#1 lore raw

PATCH v9 1/19 hw/usb/bus: Add an auto-hub flag to the USB bus

usb_claim_port() plugs a usb-hub into the last free port, so that more devices fit. Every bus gets that today, and a controller cannot turn it off.

Add auto_hub and have usb_claim_port() check it. usb_bus_new() turns it on.

No functional change.

---
 include/hw/usb/usb.h | 1 +
 hw/usb/bus.c         | 4 +++-
 2 files changed, 4 insertions(+), 1 deletion(-)
include/hw/usb/usb.h +1 -0
index cbd4711933..cc1d6d74b1 100644
@@ -484,6 +484,7 @@ struct USBBus {
     int busnr;
     int nfree;
     int nused;
+    bool auto_hub;
     QTAILQ_HEAD(, USBPort) free;
     QTAILQ_HEAD(, USBPort) used;
     QTAILQ_ENTRY(USBBus) next;
hw/usb/bus.c +3 -1
index 5cc5ffec33..92f67de4fe 100644
@@ -92,6 +92,7 @@ void usb_bus_new(USBBus *bus, size_t bus_size,
     qbus_set_bus_hotplug_handler(BUS(bus));
     bus->ops = ops;
     bus->busnr = next_usb_bus++;
+    bus->auto_hub = true;
     QTAILQ_INIT(&bus->free);
     QTAILQ_INIT(&bus->used);
     QTAILQ_INSERT_TAIL(&busses, bus, next);
@@ -413,7 +414,8 @@ void usb_claim_port(USBDevice *dev, Error **errp)
             return;
         }
     } else {
-        if (bus->nfree == 1 && strcmp(object_get_typename(OBJECT(dev)), "usb-hub") != 0) {
+        if (bus->nfree == 1 && bus->auto_hub &&
+            strcmp(object_get_typename(OBJECT(dev)), "usb-hub") != 0) {
             /* Create a new hub and chain it on */
             hub = USB_DEVICE(qdev_try_new("usb-hub"));
             if (hub) {
Signature · 1 line
2.43.0
author
#2 lore raw

PATCH v9 2/19 hw/usb/hcd-xhci: Add an auto-hub property and leave it off

In reply to Jamin Lin

xHCI has up to 15 root ports and does not need a hub to fit more devices.

Add auto-hub and leave it off. hw_compat_11_1 turns it on, so an older machine type keeps the hub it has today and a guest saved by an older QEMU still loads.

TODO: Move the entry to hw_compat_11_2 before merge. The series goes into 12.0, so 11.2 is an old machine type too. hw_compat_11_2 does not exist yet.

---
 hw/usb/hcd-xhci.h | 1 +
 hw/core/machine.c | 1 +
 hw/usb/hcd-xhci.c | 2 ++
 3 files changed, 4 insertions(+)
hw/usb/hcd-xhci.h +1 -0
index 5cb9e06d1d..a3eaf2ebb6 100644
@@ -191,6 +191,7 @@ typedef struct XHCIState {
     uint32_t numslots;
     uint32_t flags;
     uint32_t max_pstreams_mask;
+    bool auto_hub;
     void (*intr_update)(XHCIState *s, int n, bool enable);
     bool (*intr_raise)(XHCIState *s, int n, bool level);
     /*
hw/core/machine.c +1 -0
index d9ac4b419a..f8f5b01117 100644
@@ -43,6 +43,7 @@
 GlobalProperty hw_compat_11_1[] = {
     { "sysbus-ehci-usb", "x-migrate-fetch-addr-64bit", "off" },
     { "pci-ehci-usb", "x-migrate-fetch-addr-64bit", "off" },
+    { "base-xhci", "auto-hub", "on" },
 };
 const size_t hw_compat_11_1_len = G_N_ELEMENTS(hw_compat_11_1);
 
hw/usb/hcd-xhci.c +2 -0
index 376bc00264..1e42c851b4 100644
@@ -3377,6 +3377,7 @@ static void usb_xhci_init(XHCIState *xhci)
     xhci->numports = xhci->numports_2 + xhci->numports_3;
 
     usb_bus_new(&xhci->bus, sizeof(xhci->bus), &xhci_bus_ops, xhci->hostOpaque);
+    xhci->bus.auto_hub = xhci->auto_hub;
 
     for (i = 0; i < usbports; i++) {
         speedmask = 0;
@@ -3660,6 +3661,7 @@ static const Property xhci_properties[] = {
     DEFINE_PROP_UINT32("p3",    XHCIState, numports_3, 4),
     DEFINE_PROP_LINK("host",    XHCIState, hostOpaque, TYPE_DEVICE,
                      DeviceState *),
+    DEFINE_PROP_BOOL("auto-hub", XHCIState, auto_hub, false),
 };
 
 static void xhci_class_init(ObjectClass *klass, const void *data)
Signature · 1 line
2.43.0
author
#3 lore raw

PATCH v9 3/19 hw/usb/hcd-ehci: Add an auto-hub property and leave it off

In reply to Jamin Lin

Add auto-hub and leave it off. hw_compat_11_1 turns it on, so an older machine type keeps the hub it has today and a guest saved by an older QEMU still loads.

TODO: Move the entries to hw_compat_11_2 before merge. The series goes into 12.0, so 11.2 is an old machine type too. hw_compat_11_2 does not exist yet.

---
 hw/usb/hcd-ehci.h | 4 +++-
 hw/core/machine.c | 2 ++
 hw/usb/hcd-ehci.c | 1 +
 3 files changed, 6 insertions(+), 1 deletion(-)
hw/usb/hcd-ehci.h +3 -1
index b5ac9c8670..db9db537ff 100644
@@ -269,6 +269,7 @@ struct EHCIState {
     bool migrate_fetch_addr_64bit;
     bool caps_64bit_addr;
     uint32_t ctrldssegment_default;
+    bool auto_hub;
 
     /*
      *  EHCI spec version 1.0 Section 2.3
@@ -336,7 +337,8 @@ struct EHCIState {
     DEFINE_PROP_BOOL("caps-64bit-addr", _state, \
                      ehci.caps_64bit_addr, false), \
     DEFINE_PROP_UINT32("ctrldssegment-default", _state, \
-                       ehci.ctrldssegment_default, 0)
+                       ehci.ctrldssegment_default, 0), \
+    DEFINE_PROP_BOOL("auto-hub", _state, ehci.auto_hub, false)
 
 extern const VMStateDescription vmstate_ehci;
 
hw/core/machine.c +2 -0
index f8f5b01117..0103038cad 100644
@@ -43,6 +43,8 @@
 GlobalProperty hw_compat_11_1[] = {
     { "sysbus-ehci-usb", "x-migrate-fetch-addr-64bit", "off" },
     { "pci-ehci-usb", "x-migrate-fetch-addr-64bit", "off" },
+    { "sysbus-ehci-usb", "auto-hub", "on" },
+    { "pci-ehci-usb", "auto-hub", "on" },
     { "base-xhci", "auto-hub", "on" },
 };
 const size_t hw_compat_11_1_len = G_N_ELEMENTS(hw_compat_11_1);
hw/usb/hcd-ehci.c +1 -0
index f371e567f3..29b85869c3 100644
@@ -2636,6 +2636,7 @@ void usb_ehci_realize(EHCIState *s, DeviceState *dev, Error **errp)
 
     usb_bus_new(&s->bus, sizeof(s->bus), s->companion_enable ?
                 &ehci_bus_ops_companion : &ehci_bus_ops_standalone, dev);
+    s->bus.auto_hub = s->auto_hub;
     for (i = 0; i < s->portnr; i++) {
         usb_register_port(&s->bus, &s->ports[i], s, i, &ehci_port_ops,
                           USB_SPEED_MASK_HIGH);
Signature · 1 line
2.43.0
author
#4 lore raw

PATCH v9 4/19 hw/usb/hcd-uhci: Add an auto-hub property and leave it off

In reply to Jamin Lin

Add auto-hub and leave it off. hw_compat_11_1 turns it on, so an older machine type keeps the hub it has today and a guest saved by an older QEMU still loads.

A companion UHCI adds its ports to the EHCI bus instead of making one, so there the EHCI property decides. Both property lists still define auto-hub, because hw_compat_11_1 names pci-uhci-usb, and QEMU stops when a global names a property the device does not have.

TODO: Move the entry to hw_compat_11_2 before merge. The series goes into 12.0, so 11.2 is an old machine type too. hw_compat_11_2 does not exist yet.

---
 hw/usb/hcd-uhci.h | 1 +
 hw/core/machine.c | 1 +
 hw/usb/hcd-uhci.c | 3 +++
 3 files changed, 5 insertions(+)
hw/usb/hcd-uhci.h +1 -0
index 3154837f1d..06433ce3de 100644
@@ -72,6 +72,7 @@ typedef struct UHCIState {
     char *masterbus;
     uint32_t firstport;
     uint32_t maxframes;
+    bool auto_hub;
 } UHCIState;
 
 #define TYPE_UHCI "pci-uhci-usb"
hw/core/machine.c +1 -0
index 0103038cad..f79337e7d7 100644
@@ -46,6 +46,7 @@ GlobalProperty hw_compat_11_1[] = {
     { "sysbus-ehci-usb", "auto-hub", "on" },
     { "pci-ehci-usb", "auto-hub", "on" },
     { "base-xhci", "auto-hub", "on" },
+    { "pci-uhci-usb", "auto-hub", "on" },
 };
 const size_t hw_compat_11_1_len = G_N_ELEMENTS(hw_compat_11_1);
 
hw/usb/hcd-uhci.c +3 -0
index a7b9fe1317..36d4076c25 100644
@@ -1213,6 +1213,7 @@ void usb_uhci_common_realize(PCIDevice *dev, Error **errp)
         }
     } else {
         usb_bus_new(&s->bus, sizeof(s->bus), &uhci_bus_ops, DEVICE(dev));
+        s->bus.auto_hub = s->auto_hub;
         for (i = 0; i < UHCI_PORTS; i++) {
             usb_register_port(&s->bus, &s->ports[i].port, s, i, &uhci_port_ops,
                               USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL);
@@ -1260,10 +1261,12 @@ static const Property uhci_properties_companion[] = {
     DEFINE_PROP_UINT32("firstport", UHCIState, firstport, 0),
     DEFINE_PROP_UINT32("bandwidth", UHCIState, frame_bandwidth, 1280),
     DEFINE_PROP_UINT32("maxframes", UHCIState, maxframes, 128),
+    DEFINE_PROP_BOOL("auto-hub", UHCIState, auto_hub, false),
 };
 static const Property uhci_properties_standalone[] = {
     DEFINE_PROP_UINT32("bandwidth", UHCIState, frame_bandwidth, 1280),
     DEFINE_PROP_UINT32("maxframes", UHCIState, maxframes, 128),
+    DEFINE_PROP_BOOL("auto-hub", UHCIState, auto_hub, false),
 };
 
 static void uhci_class_init(ObjectClass *klass, const void *data)
Signature · 1 line
2.43.0
author
#5 lore raw

PATCH v9 5/19 hw/usb/hcd-ohci: Add an auto-hub property and leave it off

In reply to Jamin Lin

Add auto-hub and leave it off. hw_compat_11_1 turns it on for pci-ohci and sysbus-ohci, so an older machine type keeps the hub it has today and a guest saved by an older QEMU still loads.

pc87560-ohci is its own PCI front end, not a pci-ohci, so it defines the property itself.

A companion OHCI adds its ports to the EHCI bus instead of making one, so there the EHCI property decides.

TODO: Move the entries to hw_compat_11_2 before merge. The series goes into 12.0, so 11.2 is an old machine type too. hw_compat_11_2 does not exist yet.

---
 hw/usb/hcd-ohci.h        | 1 +
 hw/core/machine.c        | 2 ++
 hw/usb/hcd-ohci-pci.c    | 1 +
 hw/usb/hcd-ohci-sysbus.c | 1 +
 hw/usb/hcd-ohci.c        | 1 +
 hw/usb/pc87560-ohci.c    | 1 +
 6 files changed, 7 insertions(+)
hw/usb/hcd-ohci.h +1 -0
index d079f8d24e..43240622ef 100644
@@ -98,6 +98,7 @@ struct OHCIState {
 
     bool big_endian;
     bool consistency_check;
+    bool auto_hub;
 };
 
 #define TYPE_SYSBUS_OHCI "sysbus-ohci"
hw/core/machine.c +2 -0
index f79337e7d7..22d2b83459 100644
@@ -47,6 +47,8 @@ GlobalProperty hw_compat_11_1[] = {
     { "pci-ehci-usb", "auto-hub", "on" },
     { "base-xhci", "auto-hub", "on" },
     { "pci-uhci-usb", "auto-hub", "on" },
+    { "sysbus-ohci", "auto-hub", "on" },
+    { "pci-ohci", "auto-hub", "on" },
 };
 const size_t hw_compat_11_1_len = G_N_ELEMENTS(hw_compat_11_1);
 
hw/usb/hcd-ohci-pci.c +1 -0
index 70c9e9ac4f..11c754a948 100644
@@ -112,6 +112,7 @@ static const Property ohci_pci_properties[] = {
     DEFINE_PROP_STRING("masterbus", OHCIPCIState, masterbus),
     DEFINE_PROP_UINT32("num-ports", OHCIPCIState, num_ports, 3),
     DEFINE_PROP_UINT32("firstport", OHCIPCIState, firstport, 0),
+    DEFINE_PROP_BOOL("auto-hub", OHCIPCIState, state.auto_hub, false),
 };
 
 static const VMStateDescription vmstate_ohci = {
hw/usb/hcd-ohci-sysbus.c +1 -0
index 4f51eebccb..69f38b9fce 100644
@@ -61,6 +61,7 @@ static const Property ohci_sysbus_properties[] = {
     DEFINE_PROP_UINT32("num-ports", OHCISysBusState, num_ports, 3),
     DEFINE_PROP_UINT32("firstport", OHCISysBusState, firstport, 0),
     DEFINE_PROP_UINT64("dma-offset", OHCISysBusState, dma_offset, 0),
+    DEFINE_PROP_BOOL("auto-hub", OHCISysBusState, ohci.auto_hub, false),
 };
 
 static void ohci_sysbus_class_init(ObjectClass *klass, const void *data)
hw/usb/hcd-ohci.c +1 -0
index 5e5fe41f47..2c25829b76 100644
@@ -1987,6 +1987,7 @@ void usb_ohci_init(OHCIState *ohci, DeviceState *dev, uint32_t num_ports,
         }
     } else {
         usb_bus_new(&ohci->bus, sizeof(ohci->bus), &ohci_bus_ops, dev);
+        ohci->bus.auto_hub = ohci->auto_hub;
         for (i = 0; i < num_ports; i++) {
             usb_register_port(&ohci->bus, &ohci->rhport[i].port,
                               ohci, i, &ohci_port_ops,
hw/usb/pc87560-ohci.c +1 -0
index cc00f6e936..66c1e62d48 100644
@@ -296,6 +296,7 @@ static const Property ohci_pci_properties[] = {
     DEFINE_PROP_STRING("masterbus", OHCIPCIState, masterbus),
     DEFINE_PROP_UINT32("num-ports", OHCIPCIState, num_ports, 3),
     DEFINE_PROP_UINT32("firstport", OHCIPCIState, firstport, 0),
+    DEFINE_PROP_BOOL("auto-hub", OHCIPCIState, state.auto_hub, false),
 };
 
 static const VMStateDescription vmstate_ohci = {
Signature · 1 line
2.43.0
author
#6 lore raw

PATCH v9 6/19 hw/usb/hcd-dwc2: Add an auto-hub property and leave it off

In reply to Jamin Lin

dwc2 has one root port and runs at full speed, so a second device only fits behind the hub.

Add auto-hub and leave it off. raspi has no versioned machine type, so compat cannot reach it. bcm2835_peripherals turns the property on, and raspi keeps the hub it has today.

---
 hw/usb/hcd-dwc2.h            | 1 +
 hw/arm/bcm2835_peripherals.c | 1 +
 hw/usb/hcd-dwc2.c            | 2 ++
 3 files changed, 4 insertions(+)
hw/usb/hcd-dwc2.h +1 -0
index 68877661ca..be06f0b0e7 100644
@@ -167,6 +167,7 @@ struct DWC2State {
     uint16_t fi;
     uint16_t next_chan;
     bool working;
+    bool auto_hub;
     USBPort uport;
     DWC2Packet packet[DWC2_NB_CHAN];                   /* one packet per chan */
     uint8_t usb_buf[DWC2_NB_CHAN][DWC2_MAX_XFER_SIZE]; /* one buffer per chan */
hw/arm/bcm2835_peripherals.c +1 -0
index 558c180df9..9187376812 100644
@@ -152,6 +152,7 @@ static void raspi_peripherals_base_init(Object *obj)
 
     /* DWC2 */
     object_initialize_child(obj, "dwc2", &s->dwc2, TYPE_DWC2_USB);
+    qdev_prop_set_bit(DEVICE(&s->dwc2), "auto-hub", true);
 
     /* CPRMAN clock manager */
     object_initialize_child(obj, "cprman", &s->cprman, TYPE_BCM2835_CPRMAN);
hw/usb/hcd-dwc2.c +2 -0
index cd3b61158d..14211e6413 100644
@@ -1354,6 +1354,7 @@ static void dwc2_realize(DeviceState *dev, Error **errp)
     address_space_init(&s->dma_as, s->dma_mr, "dwc2");
 
     usb_bus_new(&s->bus, sizeof(s->bus), &dwc2_bus_ops, dev);
+    s->bus.auto_hub = s->auto_hub;
     usb_register_port(&s->bus, &s->uport, s, 0, &dwc2_port_ops,
                       USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL |
                       (s->usb_version == 2 ? USB_SPEED_MASK_HIGH : 0));
@@ -1450,6 +1451,7 @@ const VMStateDescription vmstate_dwc2_state = {
 
 static const Property dwc2_usb_properties[] = {
     DEFINE_PROP_UINT32("usb_version", DWC2State, usb_version, 2),
+    DEFINE_PROP_BOOL("auto-hub", DWC2State, auto_hub, false),
 };
 
 static void dwc2_class_init(ObjectClass *klass, const void *data)
Signature · 1 line
2.43.0
author
#7 lore raw

PATCH v9 7/19 hw/usb/xen-usb: Do not insert an automatic hub

In reply to Jamin Lin

xen-usb gives every device a port number. usb_claim_port() then uses that port, and never reaches the automatic hub.

Set auto_hub to false. Every bus creator then sets the flag itself, and usb_bus_new() can stop setting a default.

---
 hw/usb/xen-usb.c | 1 +
 1 file changed, 1 insertion(+)
hw/usb/xen-usb.c +1 -0
index caf9efbb77..5d1dbe6b1b 100644
@@ -1023,6 +1023,7 @@ static void usbback_alloc(struct XenLegacyDevice *xendev)
 
     usb_bus_new(&usbif->bus, sizeof(usbif->bus), &xen_usb_bus_ops,
                 DEVICE(xendev));
+    usbif->bus.auto_hub = false;
     for (i = 0; i < USBBACK_MAXPORTS; i++) {
         p = &(usbif->ports[i].port);
         usb_register_port(&usbif->bus, p, usbif, i, &xen_usb_port_ops,
Signature · 1 line
2.43.0
author
#8 lore raw

PATCH v9 8/19 hw/arm/npcm7xx: Keep the automatic hub

In reply to Jamin Lin

The OHCI companion has one port, so a second device only fits behind the hub.

npcm7xx has no versioned machine type, so compat cannot reach it. Turn the property on here instead. The companion shares the EHCI bus, so the EHCI carries the property.

---
 hw/arm/npcm7xx.c | 2 ++
 1 file changed, 2 insertions(+)
hw/arm/npcm7xx.c +2 -0
index 695f30a0dc..3f5b33d7e3 100644
@@ -628,6 +628,8 @@ static void npcm7xx_realize(DeviceState *dev, Error **errp)
     /* USB Host */
     object_property_set_bool(OBJECT(&s->ehci), "companion-enable", true,
                              &error_abort);
+    object_property_set_bool(OBJECT(&s->ehci), "auto-hub", true,
+                             &error_abort);
     sysbus_realize(SYS_BUS_DEVICE(&s->ehci), &error_abort);
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->ehci), 0, NPCM7XX_EHCI_BA);
     sysbus_connect_irq(SYS_BUS_DEVICE(&s->ehci), 0,
Signature · 1 line
2.43.0
author
#9 lore raw

PATCH v9 9/19 hw/usb/bus: Turn off the automatic hub by default

In reply to Jamin Lin

Every bus creator now sets auto_hub from its own property, or clears it outright, so the value usb_bus_new() leaves there is never read.

Drop the line. A new bus then has no hub unless its controller asks for one.

---
 hw/usb/bus.c | 1 -
 1 file changed, 1 deletion(-)
hw/usb/bus.c +0 -1
index 92f67de4fe..cd587db892 100644
@@ -92,7 +92,6 @@ void usb_bus_new(USBBus *bus, size_t bus_size,
     qbus_set_bus_hotplug_handler(BUS(bus));
     bus->ops = ops;
     bus->busnr = next_usb_bus++;
-    bus->auto_hub = true;
     QTAILQ_INIT(&bus->free);
     QTAILQ_INIT(&bus->used);
     QTAILQ_INSERT_TAIL(&busses, bus, next);
Signature · 1 line
2.43.0
author
#10 lore raw

PATCH v9 10/19 docs/system/devices/usb: Note the automatic hub change

In reply to Jamin Lin

The automatic hub is now off for a new machine type. Say how to get a hub back: plug in a usb-hub, or turn auto-hub back on for the controller.

TODO: The text says 12.0. That is right once the compat entries move to hw_compat_11_2.

---
 docs/system/devices/usb.rst | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)
docs/system/devices/usb.rst +15 -2
index dc694d23c2..dc73c4048e 100644
@@ -3,8 +3,21 @@ USB emulation
 
 QEMU can emulate a PCI UHCI, OHCI, EHCI or XHCI USB controller. You can
 plug virtual USB devices or real host USB devices (only works with
-certain host operating systems). QEMU will automatically create and
-connect virtual USB hubs as necessary to connect multiple USB devices.
+certain host operating systems).
+
+A machine type older than 12.0 plugs a virtual USB hub into the last
+free port, so that more devices fit. A newer one does not, and a
+device that finds no free port fails.
+
+There are two ways to get a hub back. One is to add a hub yourself.
+Put it on the command line before the other USB devices::
+
+    -device usb-hub
+
+The other is to turn the automatic hub back on. Use the name of your
+USB controller, for example::
+
+    -global piix3-usb-uhci.auto-hub=on
 
 USB controllers
 ~~~~~~~~~~~~~~~
Signature · 1 line
2.43.0
author
#11 lore raw

PATCH v9 11/19 hw/usb/redirect-server: Add a usbredir server device

In reply to Jamin Lin

Add "x-usb-redir-server". It exports a USB device from this QEMU to a USB host in another QEMU, using the usbredir protocol. The other side is the existing "usb-redir" device.

The name has an "x-" prefix because the device is experimental. It has to sit on the sysbus so that qdev can find the USB bus it provides, and that may change later, so the name is not fixed yet.

The exported device is plugged in with "-device <dev>,bus=<id>.0". So we have to provide a USB bus. To find that bus, qdev starts at the main system bus and walks down the tree. A device that is not on a bus is not in the tree, so the bus it provides cannot be found. We have to sit on the main system bus. That makes this a sysbus device, even though it has no MMIO and no IRQ.

usbredir carries one device, not a bus. A hub cannot be exported because the protocol has no device address field, so only the device on the first port is redirected. To export several devices, run one x-usb-redir-server per device, each with its own chardev.

The link to the remote host is a chardev, so this device cannot be migrated. Its VMStateDescription says so with unmigratable.

---
 hw/usb/redirect-server.c | 143 +++++++++++++++++++++++++++++++++++++++
 hw/usb/meson.build       |   3 +-
 2 files changed, 145 insertions(+), 1 deletion(-)
 create mode 100644 hw/usb/redirect-server.c
hw/usb/redirect-server.c new file +143 -0
new file mode 100644
index 0000000000..1423564d22
@@ -0,0 +1,143 @@
+/*
+ * USB redirector, server side
+ *
+ * Copyright (c) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Exports a locally emulated USB device to a remote USB host over the
+ * usbredir protocol, so a device emulated in one QEMU instance can be
+ * enumerated by a host controller emulated in another one.
+ *
+ * Architecture. The left column is a request going to the device. The
+ * right column is the answer coming back. The middle hop carries
+ * usbredir messages over a socket. The top and bottom hops carry
+ * USBPackets inside QEMU.
+ *
+ *     remote QEMU: guest driver -> EHCI/XHCI
+ *              |                     ^
+ *    USBPacket |                     | USBPacket
+ *              v                     |
+ *     "usb-redir" (the client)
+ *              |                     ^
+ *     usbredir |   chardev socket    | usbredir
+ *              v                     |
+ *     x-usb-redir-server (the server, this file)
+ *              |                     ^
+ *    USBPacket |                     | USBPacket
+ *              v                     |
+ *     any USBDevice, "-device <dev>,bus=<id>.0"
+ *
+ * "usb-redir" (hw/usb/redirect.c) is the client:
+ *   - it takes a USBPacket from the remote guest and writes it to the
+ *     socket as a usbredir message
+ *   - it reads the answer from the socket and completes the USBPacket
+ *
+ * This file is the server. It does the same thing, but backwards:
+ *   - it reads a usbredir message from the socket and runs it as a
+ *     USBPacket on the bus below
+ *   - it takes the result of that USBPacket and writes it back to the
+ *     same socket as a usbredir message, for the client to read
+ *
+ * A USB device has to sit on a USB bus, and in QEMU a USB bus is always
+ * made by a host controller. So this file makes one and acts as the
+ * host controller on this side. It models no real chip: its cable is
+ * the chardev socket. The real host is in the other QEMU.
+ *
+ * usbredir carries one device, not a bus. A hub cannot be exported:
+ * the protocol has no device address field. To export several devices,
+ * run one x-usb-redir-server per device, each with its own chardev.
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qemu/module.h"
+#include "migration/vmstate.h"
+#include "hw/core/sysbus.h"
+#include "hw/usb/usb.h"
+#include "qom/object.h"
+
+#define TYPE_USB_REDIR_SERVER "x-usb-redir-server"
+OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
+
+struct USBRedirServer {
+    SysBusDevice parent_obj;
+
+    /* USB bus */
+    USBBus bus;
+    USBPort port;
+};
+
+/*
+ * USB port ops
+ */
+
+static void usbredir_server_port_child_detach(USBPort *port, USBDevice *child)
+{
+    /* We only export the device on our own port. Nothing to do. */
+}
+
+static void usbredir_server_port_wakeup(USBPort *port)
+{
+    /* We do not pass remote wakeup to the host. Nothing to do. */
+}
+
+static USBPortOps usbredir_server_port_ops = {
+    .child_detach = usbredir_server_port_child_detach,
+    .wakeup = usbredir_server_port_wakeup,
+};
+
+/*
+ * USB bus ops
+ */
+
+static USBBusOps usbredir_server_bus_ops = {
+};
+
+/*
+ * Device registration
+ */
+
+static void usbredir_server_realize(DeviceState *dev, Error **errp)
+{
+    USBRedirServer *s = USB_REDIR_SERVER(dev);
+
+    /* One port: usbredir carries a single device. */
+    usb_bus_new(&s->bus, sizeof(s->bus), &usbredir_server_bus_ops, dev);
+    s->bus.auto_hub = false;
+    usb_register_port(&s->bus, &s->port, s, 0, &usbredir_server_port_ops,
+                      USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL |
+                      USB_SPEED_MASK_HIGH);
+}
+
+/*
+ * The link to the remote host is a chardev, and we cannot migrate
+ * that. So this device cannot be migrated either.
+ */
+static const VMStateDescription vmstate_usbredir_server = {
+    .name = TYPE_USB_REDIR_SERVER,
+    .unmigratable = 1,
+};
+
+static void usbredir_server_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->desc = "USB Redirection Server";
+    dc->realize = usbredir_server_realize;
+    dc->vmsd = &vmstate_usbredir_server;
+    set_bit(DEVICE_CATEGORY_USB, dc->categories);
+}
+
+static const TypeInfo usbredir_server_types[] = {
+    {
+        .name = TYPE_USB_REDIR_SERVER,
+        .parent = TYPE_DYNAMIC_SYS_BUS_DEVICE,
+        .instance_size = sizeof(USBRedirServer),
+        .class_init = usbredir_server_class_init,
+    },
+};
+module_obj(TYPE_USB_REDIR_SERVER);
+module_kconfig(USB);
+
+DEFINE_TYPES(usbredir_server_types)
hw/usb/meson.build +2 -1
index 429dcd6a24..0061b935e6 100644
@@ -78,7 +78,8 @@ endif
 if usbredir.found()
   usbredir_ss = ss.source_set()
   usbredir_ss.add(when: 'CONFIG_USB',
-                  if_true: [usbredir, files('redirect.c', 'quirks.c')])
+                  if_true: [usbredir, files('redirect.c', 'quirks.c',
+                                            'redirect-server.c')])
   hw_usb_modules += {'redirect': usbredir_ss}
 endif
 
Signature · 1 line
2.43.0
author
#12 lore raw

PATCH v9 12/19 hw/usb/redirect-server: Connect usbredirparser to a chardev

In reply to Jamin Lin

Create a usbredirparser when the chardev opens. Feed it from the chardev read handler. Destroy it when the chardev closes.

The side that exports a device sets usbredirparser_fl_usb_host. This side drives the device, so it is the USB host for it. redirect.c is the other side and does not set the flag. The library uses it to decide which direction a packet may travel.

The chardev handling follows hw/usb/redirect.c: take no data before the parser exists, do not write to a closed backend, do not re-enter the writer, and let a close that is still queued finish before a new connection opens.

---
 hw/usb/redirect-server.c | 289 +++++++++++++++++++++++++++++++++++++++
 hw/usb/trace-events      |   6 +
 2 files changed, 295 insertions(+)
hw/usb/redirect-server.c +289 -0
index 1423564d22..5abaf7e01e 100644
@@ -50,22 +50,46 @@
  */
 
 #include "qemu/osdep.h"
+#include "qemu/units.h"
 #include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "qemu/main-loop.h"
 #include "qemu/module.h"
 #include "migration/vmstate.h"
 #include "hw/core/sysbus.h"
 #include "hw/usb/usb.h"
+#include "chardev/char-fe.h"
 #include "qom/object.h"
 
+#include <usbredirparser.h>
+
+#include "hw/core/qdev-properties.h"
+#include "hw/core/qdev-properties-system.h"
+#include "trace.h"
+
 #define TYPE_USB_REDIR_SERVER "x-usb-redir-server"
 OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
 
+#define USBREDIR_SERVER_VERSION "qemu " TYPE_USB_REDIR_SERVER " " QEMU_VERSION
+
 struct USBRedirServer {
     SysBusDevice parent_obj;
 
     /* USB bus */
     USBBus bus;
     USBPort port;
+
+    /* Properties */
+    CharFrontend cs;
+
+    /* usbredir over the chardev */
+    struct usbredirparser *parser;
+    QEMUBH *chardev_close_bh;
+    const uint8_t *read_buf;
+    int read_buf_size;
+    bool in_write;
+    guint watch;
+    bool host_connected;
 };
 
 /*
@@ -94,6 +118,237 @@ static USBPortOps usbredir_server_port_ops = {
 static USBBusOps usbredir_server_bus_ops = {
 };
 
+/*
+ * usbredirparser I/O and logging callbacks
+ */
+
+static void usbredir_server_log(void *priv, int level, const char *msg)
+{
+    switch (level) {
+    case usbredirparser_error:
+        error_report(TYPE_USB_REDIR_SERVER ": %s", msg);
+        break;
+    case usbredirparser_warning:
+        warn_report(TYPE_USB_REDIR_SERVER ": %s", msg);
+        break;
+    default:
+        trace_usbredir_server_log(msg);
+        break;
+    }
+}
+
+static int usbredir_server_read(void *priv, uint8_t *data, int count)
+{
+    USBRedirServer *s = priv;
+
+    if (s->read_buf_size < count) {
+        count = s->read_buf_size;
+    }
+
+    memcpy(data, s->read_buf, count);
+
+    s->read_buf_size -= count;
+    if (s->read_buf_size) {
+        s->read_buf += count;
+    } else {
+        s->read_buf = NULL;
+    }
+
+    return count;
+}
+
+static gboolean usbredir_server_write_unblocked(void *do_not_use,
+                                                GIOCondition cond,
+                                                void *opaque)
+{
+    USBRedirServer *s = opaque;
+
+    s->watch = 0;
+    usbredirparser_do_write(s->parser);
+
+    return G_SOURCE_REMOVE;
+}
+
+static int usbredir_server_write(void *priv, uint8_t *data, int count)
+{
+    USBRedirServer *s = priv;
+    int ret;
+
+    if (!qemu_chr_fe_backend_open(&s->cs)) {
+        return 0;
+    }
+
+    /*
+     * Re-entry guard. The chain is:
+     *   usbredirparser_do_write() -> this -> qemu_chr_fe_write()
+     *   -> chardev feeds us -> usbredirparser_do_read()
+     *   -> a callback -> usbredirparser_do_write() again
+     *
+     * The second usbredirparser_do_write() would walk the same buffer
+     * queue as the first. Returning 0 means "sent nothing", so the
+     * buffer stays for the outer one to send.
+     */
+    if (s->in_write) {
+        trace_usbredir_server_write_recursion();
+        return 0;
+    }
+    s->in_write = true;
+
+    ret = qemu_chr_fe_write(&s->cs, data, count);
+    if (ret < count) {
+        if (!s->watch) {
+            s->watch = qemu_chr_fe_add_watch(&s->cs, G_IO_OUT | G_IO_HUP,
+                                             usbredir_server_write_unblocked,
+                                             s);
+        }
+        if (ret < 0) {
+            ret = 0;
+        }
+    }
+
+    s->in_write = false;
+
+    return ret;
+}
+
+/* The remote host greets us once the socket is up. */
+static void usbredir_server_hello(void *priv,
+                                  struct usb_redir_hello_header *hello)
+{
+    USBRedirServer *s = priv;
+
+    s->host_connected = true;
+}
+
+/*
+ * Parser setup and teardown
+ */
+
+static void usbredir_server_create_parser(USBRedirServer *s)
+{
+    uint32_t caps[USB_REDIR_CAPS_SIZE] = {};
+
+    s->parser = usbredirparser_create();
+    if (!s->parser) {
+        error_report(TYPE_USB_REDIR_SERVER ": failed to create usbredirparser");
+        return;
+    }
+
+    s->parser->priv = s;
+    s->parser->log_func = usbredir_server_log;
+    s->parser->read_func = usbredir_server_read;
+    s->parser->write_func = usbredir_server_write;
+
+    /* Callbacks for messages the remote host sends to us */
+    s->parser->hello_func = usbredir_server_hello;
+
+    /* Capabilities: 64-bit IDs, connect_device_version, ep_info sizes */
+    usbredirparser_caps_set_cap(caps, usb_redir_cap_connect_device_version);
+    usbredirparser_caps_set_cap(caps, usb_redir_cap_ep_info_max_packet_size);
+    usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids);
+
+    /*
+     * In USB the host is the side that starts every transfer; a device
+     * only answers. The exported device sits on our bus and we issue
+     * its transfers, so for that device we are the host. That is what
+     * fl_usb_host means, and why the side that exports a device sets it.
+     *
+     * The other QEMU does not drive the device, it receives it. In this
+     * protocol that makes it the client, and redirect.c leaves the flag
+     * clear.
+     *
+     * Without it the library refuses to send device_connect.
+     */
+    usbredirparser_init(s->parser, USBREDIR_SERVER_VERSION,
+                        caps, USB_REDIR_CAPS_SIZE,
+                        usbredirparser_fl_usb_host);
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_destroy_parser(USBRedirServer *s)
+{
+    s->host_connected = false;
+
+    g_clear_handle_id(&s->watch, g_source_remove);
+
+    if (s->parser) {
+        usbredirparser_destroy(s->parser);
+        s->parser = NULL;
+    }
+}
+
+static void usbredir_server_chardev_close_bh(void *opaque)
+{
+    usbredir_server_destroy_parser(opaque);
+}
+
+/*
+ * chardev callbacks
+ */
+
+static int usbredir_server_chardev_can_read(void *opaque)
+{
+    USBRedirServer *s = opaque;
+
+    if (!s->parser) {
+        return 0;
+    }
+    /* usbredirparser_do_read() consumes everything we hand it */
+    return 1 * MiB;
+}
+
+static void usbredir_server_chardev_read(void *opaque, const uint8_t *buf,
+                                         int size)
+{
+    USBRedirServer *s = opaque;
+
+    if (!s->parser) {
+        return;
+    }
+
+    /* No recursion allowed */
+    assert(s->read_buf == NULL);
+
+    s->read_buf = buf;
+    s->read_buf_size = size;
+
+    usbredirparser_do_read(s->parser);
+    /*
+     * usbredirparser_do_read() ran our callbacks. Flush whatever
+     * replies they queued.
+     */
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_chardev_event(void *opaque,
+                                          QEMUChrEvent event)
+{
+    USBRedirServer *s = opaque;
+
+    switch (event) {
+    case CHR_EVENT_OPENED:
+        trace_usbredir_server_chardev_open();
+        /*
+         * A close event only schedules chardev_close_bh. If it has not
+         * run yet, it would destroy the parser we are about to create,
+         * so run it now and cancel it.
+         */
+        usbredir_server_chardev_close_bh(s);
+        qemu_bh_cancel(s->chardev_close_bh);
+        usbredir_server_create_parser(s);
+        break;
+    case CHR_EVENT_CLOSED:
+        trace_usbredir_server_chardev_close();
+        qemu_bh_schedule(s->chardev_close_bh);
+        break;
+    case CHR_EVENT_BREAK:
+    case CHR_EVENT_MUX_IN:
+    case CHR_EVENT_MUX_OUT:
+    case CHR_EVENT_RESIZE:
+        break;
+    }
+}
+
 /*
  * Device registration
  */
@@ -102,14 +357,46 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
 {
     USBRedirServer *s = USB_REDIR_SERVER(dev);
 
+    if (!qemu_chr_fe_backend_connected(&s->cs)) {
+        error_setg(errp,
+                   TYPE_USB_REDIR_SERVER ": 'chardev' property must be set");
+        return;
+    }
+
     /* One port: usbredir carries a single device. */
     usb_bus_new(&s->bus, sizeof(s->bus), &usbredir_server_bus_ops, dev);
     s->bus.auto_hub = false;
     usb_register_port(&s->bus, &s->port, s, 0, &usbredir_server_port_ops,
                       USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL |
                       USB_SPEED_MASK_HIGH);
+
+    s->chardev_close_bh = qemu_bh_new_guarded(usbredir_server_chardev_close_bh,
+                                              s, &dev->mem_reentrancy_guard);
+
+    qemu_chr_fe_set_handlers(&s->cs,
+                             usbredir_server_chardev_can_read,
+                             usbredir_server_chardev_read,
+                             usbredir_server_chardev_event,
+                             NULL, s, NULL, true);
+}
+
+static void usbredir_server_unrealize(DeviceState *dev)
+{
+    USBRedirServer *s = USB_REDIR_SERVER(dev);
+
+    qemu_chr_fe_deinit(&s->cs, true);
+    usbredir_server_destroy_parser(s);
+
+    if (s->chardev_close_bh) {
+        qemu_bh_delete(s->chardev_close_bh);
+        s->chardev_close_bh = NULL;
+    }
 }
 
+static const Property usbredir_server_props[] = {
+    DEFINE_PROP_CHR("chardev", USBRedirServer, cs),
+};
+
 /*
  * The link to the remote host is a chardev, and we cannot migrate
  * that. So this device cannot be migrated either.
@@ -125,8 +412,10 @@ static void usbredir_server_class_init(ObjectClass *klass, const void *data)
 
     dc->desc = "USB Redirection Server";
     dc->realize = usbredir_server_realize;
+    dc->unrealize = usbredir_server_unrealize;
     dc->vmsd = &vmstate_usbredir_server;
     set_bit(DEVICE_CATEGORY_USB, dc->categories);
+    device_class_set_props(dc, usbredir_server_props);
 }
 
 static const TypeInfo usbredir_server_types[] = {
hw/usb/trace-events +6 -0
index 80ead23358..d141661673 100644
@@ -393,3 +393,9 @@ aspeed_udc_handle_data(int ep_nr, const char *dir, uint32_t iov, int ep_idx) "ep
 aspeed_udc_ep_data_in(unsigned ep, uint32_t rptr, uint32_t wptr, uint32_t iov) "ep %u, rptr %u, wptr %u, iov %u"
 aspeed_udc_ep_data_out(unsigned ep, uint32_t wptr, uint32_t avail, uint32_t iov) "ep %u, wptr %u, avail %u, iov %u"
 aspeed_udc_ep_ack(unsigned ep) "ep %u"
+
+# redirect-server.c
+usbredir_server_chardev_open(void) "chardev opened"
+usbredir_server_chardev_close(void) "chardev closed"
+usbredir_server_write_recursion(void) "recursive write, leaving it queued"
+usbredir_server_log(const char *msg) "%s"
Signature · 1 line
2.43.0
author
#13 lore raw

PATCH v9 13/19 hw/usb/redirect-server: Announce the exported device

In reply to Jamin Lin

Send device_connect when a device is attached and the host has greeted us, device_disconnect when it goes away, and handle the bus reset the host sends before enumerating.

device_connect carries no identity. The peer asks the device itself with GET_DESCRIPTOR as soon as it enumerates, so anything we put there would be made up. The speed is real: it comes from the device on our port.

---
 hw/usb/redirect-server.c | 136 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |   6 ++
 2 files changed, 141 insertions(+), 1 deletion(-)
hw/usb/redirect-server.c +135 -1
index 5abaf7e01e..5a3b01c337 100644
@@ -56,6 +56,7 @@
 #include "qemu/main-loop.h"
 #include "qemu/module.h"
 #include "migration/vmstate.h"
+#include "qemu/cutils.h"
 #include "hw/core/sysbus.h"
 #include "hw/usb/usb.h"
 #include "chardev/char-fe.h"
@@ -85,17 +86,91 @@ struct USBRedirServer {
     /* usbredir over the chardev */
     struct usbredirparser *parser;
     QEMUBH *chardev_close_bh;
+    QEMUBH *announce_bh;
     const uint8_t *read_buf;
     int read_buf_size;
     bool in_write;
     guint watch;
     bool host_connected;
+    bool device_announced;
 };
 
+/*
+ * The device is whatever USBDevice the user plugged into our port with
+ * "-device <device>,bus=<id>.0". NULL until then.
+ */
+static USBDevice *usbredir_server_device(USBRedirServer *s)
+{
+    return s->port.dev;
+}
+
+/*
+ * Device announcement
+ */
+
+static uint8_t usbredir_server_speed(USBDevice *device)
+{
+    switch (device->speed) {
+    case USB_SPEED_LOW:
+        return usb_redir_speed_low;
+    case USB_SPEED_FULL:
+        return usb_redir_speed_full;
+    default:
+        return usb_redir_speed_high;
+    }
+}
+
+static void usbredir_server_announce_bh(void *opaque)
+{
+    USBRedirServer *s = opaque;
+    USBDevice *device = usbredir_server_device(s);
+    struct usb_redir_device_connect_header conn = {};
+
+    if (!s->host_connected || s->device_announced ||
+        !device || !device->attached) {
+        return;
+    }
+    s->device_announced = true;
+    conn.speed = usbredir_server_speed(device);
+
+    usbredirparser_send_device_connect(s->parser, &conn);
+    usbredirparser_do_write(s->parser);
+}
+
 /*
  * USB port ops
  */
 
+static void usbredir_server_port_attach(USBPort *port)
+{
+    USBRedirServer *s = port->opaque;
+
+    trace_usbredir_server_attach();
+
+    /*
+     * usb_attach() sets the state to ATTACHED after this returns.
+     * A device needs DEFAULT state before it can serve a request.
+     * The BH runs after usb_attach(), so the device stays in DEFAULT.
+     */
+    qemu_bh_schedule(s->announce_bh);
+}
+
+static void usbredir_server_port_detach(USBPort *port)
+{
+    USBRedirServer *s = port->opaque;
+
+    trace_usbredir_server_detach(s->device_announced);
+
+    qemu_bh_cancel(s->announce_bh);
+
+    if (s->host_connected && s->parser && s->device_announced) {
+        trace_usbredir_server_disconnect();
+        usbredirparser_send_device_disconnect(s->parser);
+        usbredirparser_do_write(s->parser);
+    }
+    s->device_announced = false;
+}
+
 static void usbredir_server_port_child_detach(USBPort *port, USBDevice *child)
 {
     /* We only export the device on our own port. Nothing to do. */
@@ -107,6 +182,8 @@ static void usbredir_server_port_wakeup(USBPort *port)
 }
 
 static USBPortOps usbredir_server_port_ops = {
+    .attach = usbredir_server_port_attach,
+    .detach = usbredir_server_port_detach,
     .child_detach = usbredir_server_port_child_detach,
     .wakeup = usbredir_server_port_wakeup,
 };
@@ -211,13 +288,53 @@ static int usbredir_server_write(void *priv, uint8_t *data, int count)
     return ret;
 }
 
-/* The remote host greets us once the socket is up. */
+/*
+ * usbredirparser message callbacks
+ */
+
 static void usbredir_server_hello(void *priv,
                                   struct usb_redir_hello_header *hello)
 {
     USBRedirServer *s = priv;
+    char version[sizeof(hello->version) + 1];
+
+    pstrcpy(version, sizeof(version), hello->version);
+    trace_usbredir_server_hello(version);
 
     s->host_connected = true;
+    qemu_bh_schedule(s->announce_bh);
+}
+
+static void usbredir_server_reset(void *priv)
+{
+    USBRedirServer *s = priv;
+    USBDevice *device = usbredir_server_device(s);
+
+    trace_usbredir_server_bus_reset(device && device->attached);
+    usb_device_reset(device);
+}
+
+static void usbredir_server_filter_reject(void *priv)
+{
+    trace_usbredir_server_filter_reject();
+}
+
+static void usbredir_server_filter_filter(void *priv,
+    struct usbredirfilter_rule *rules, int rules_count)
+{
+    /* We accept any host. The callback owns the rules, so free them. */
+    free(rules);
+}
+
+static void usbredir_server_device_disconnect_ack(void *priv)
+{
+    /* The host saw our device_disconnect. Nothing to do. */
+}
+
+static void usbredir_server_interface_info(void *priv,
+    struct usb_redir_interface_info_header *hdr)
+{
+    /* The host should not send this to a device. Nothing to do. */
 }
 
 /*
@@ -241,6 +358,14 @@ static void usbredir_server_create_parser(USBRedirServer *s)
 
     /* Callbacks for messages the remote host sends to us */
     s->parser->hello_func = usbredir_server_hello;
+    s->parser->reset_func = usbredir_server_reset;
+
+    /* The parser calls these directly, so they must not be NULL. */
+    s->parser->filter_reject_func = usbredir_server_filter_reject;
+    s->parser->filter_filter_func = usbredir_server_filter_filter;
+    s->parser->device_disconnect_ack_func =
+        usbredir_server_device_disconnect_ack;
+    s->parser->interface_info_func = usbredir_server_interface_info;
 
     /* Capabilities: 64-bit IDs, connect_device_version, ep_info sizes */
     usbredirparser_caps_set_cap(caps, usb_redir_cap_connect_device_version);
@@ -268,7 +393,9 @@ static void usbredir_server_create_parser(USBRedirServer *s)
 static void usbredir_server_destroy_parser(USBRedirServer *s)
 {
     s->host_connected = false;
+    s->device_announced = false;
 
+    qemu_bh_cancel(s->announce_bh);
     g_clear_handle_id(&s->watch, g_source_remove);
 
     if (s->parser) {
@@ -372,6 +499,8 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
 
     s->chardev_close_bh = qemu_bh_new_guarded(usbredir_server_chardev_close_bh,
                                               s, &dev->mem_reentrancy_guard);
+    s->announce_bh = qemu_bh_new_guarded(usbredir_server_announce_bh, s,
+                                         &dev->mem_reentrancy_guard);
 
     qemu_chr_fe_set_handlers(&s->cs,
                              usbredir_server_chardev_can_read,
@@ -391,6 +520,11 @@ static void usbredir_server_unrealize(DeviceState *dev)
         qemu_bh_delete(s->chardev_close_bh);
         s->chardev_close_bh = NULL;
     }
+
+    if (s->announce_bh) {
+        qemu_bh_delete(s->announce_bh);
+        s->announce_bh = NULL;
+    }
 }
 
 static const Property usbredir_server_props[] = {
hw/usb/trace-events +6 -0
index d141661673..075a9d9eae 100644
@@ -399,3 +399,9 @@ usbredir_server_chardev_open(void) "chardev opened"
 usbredir_server_chardev_close(void) "chardev closed"
 usbredir_server_write_recursion(void) "recursive write, leaving it queued"
 usbredir_server_log(const char *msg) "%s"
+usbredir_server_hello(const char *version) "peer is %s"
+usbredir_server_attach(void) "device attached"
+usbredir_server_detach(bool announced) "device detached, was announced %d"
+usbredir_server_disconnect(void) "device_disconnect sent"
+usbredir_server_bus_reset(bool attached) "bus reset, attached %d"
+usbredir_server_filter_reject(void) "host rejected our device"
Signature · 1 line
2.43.0
author
#14 lore raw

PATCH v9 14/19 hw/usb/redirect-server: Implement control transfers

In reply to Jamin Lin

A usbredir control_packet carries a whole control transfer, so hand it to the device as one packet with the eight setup bytes in USBPacket::parameter.

The device may answer later, so we have to remember the packets we sent. Each one stays on a list until it comes back. We drop the whole list on a bus reset or a chardev close. When the host asks us to cancel a packet, we answer it and drop it. If it is no longer on the list we already answered it, so we send nothing.

A device can answer USB_RET_NAK, which means the endpoint has nothing right now, not that the transfer failed. Keep the request and send it again when the device wakes the endpoint. Not every device wakes it, so a timer asks again as well.

Every request is checked before we act on it, and one we turn away is still answered, so the host never waits for ever.

usbredir has its own messages for set_configuration, get_configuration, set_alt_setting and get_alt_setting. Each one is a control transfer, so run it like one and send back the status message.

usbredir wants an ep_info message. It says what kind each endpoint is, how big its packets are and which interface it belongs to. QEMU cannot tell us. It only knows after it has read the device's descriptors, and it never reads them for a device that just forwards transfers. So we read them ourselves: the configuration descriptor passes through us on its way to the host.

A device can offer several configurations, and an interface several alternate settings, each with its own endpoints. Keep every configuration descriptor, build the table from the one the host selected, and send a fresh ep_info when that selection changes, so ep_info never describes an endpoint that is not there.

---
 hw/usb/redirect-server.c | 921 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |  12 +
 2 files changed, 932 insertions(+), 1 deletion(-)
hw/usb/redirect-server.c +920 -1
index 5a3b01c337..89d88d6457 100644
@@ -56,6 +56,7 @@
 #include "qemu/main-loop.h"
 #include "qemu/module.h"
 #include "migration/vmstate.h"
+#include "qemu/timer.h"
 #include "qemu/cutils.h"
 #include "hw/core/sysbus.h"
 #include "hw/usb/usb.h"
@@ -63,9 +64,11 @@
 #include "qom/object.h"
 
 #include <usbredirparser.h>
+#include <usbredirproto.h>
 
 #include "hw/core/qdev-properties.h"
 #include "hw/core/qdev-properties-system.h"
+#include "desc.h"
 #include "trace.h"
 
 #define TYPE_USB_REDIR_SERVER "x-usb-redir-server"
@@ -73,6 +76,90 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
 
 #define USBREDIR_SERVER_VERSION "qemu " TYPE_USB_REDIR_SERVER " " QEMU_VERSION
 
+/*
+ * The usbredir ep_info message has one slot per endpoint and direction.
+ * Index 0-15 are the OUT endpoints, 16-31 the IN ones.
+ */
+#define USBREDIR_SERVER_MAX_EP 32
+#define USBREDIR_SERVER_EP_IN_BASE 16
+
+/* The size of each kind of descriptor. */
+#define USBREDIR_SERVER_DESC_CONFIG_SIZE 9
+#define USBREDIR_SERVER_DESC_INTERFACE_SIZE 9
+#define USBREDIR_SERVER_DESC_ENDPOINT_SIZE 7
+
+/*
+ * Ask again this often after an endpoint answered NAK. Most devices
+ * wake the endpoint when data arrives, and then we ask at once. This
+ * timer is for the devices that do not.
+ */
+#define USBREDIR_SERVER_RETRY_MS 1000
+
+typedef enum {
+    USBREDIR_SERVER_CTRL,
+} USBRedirServerPktType;
+
+/* Which message answers the host when a control transfer ends. */
+typedef enum {
+    USBREDIR_SERVER_REPLY_CONTROL,
+    USBREDIR_SERVER_REPLY_CONFIG,
+    USBREDIR_SERVER_REPLY_ALT,
+} USBRedirServerReply;
+
+/* What the device said when usbredir_server_ask_device() asked it. */
+typedef enum {
+    /* It took the request and will call back. */
+    USBREDIR_SERVER_ASK_ASYNC,
+    /* It has nothing now, so the request went back on the list. */
+    USBREDIR_SERVER_ASK_NAK,
+    /* It answered, and the request is freed. */
+    USBREDIR_SERVER_ASK_COMPLETE,
+} USBRedirServerAsk;
+
+typedef struct USBRedirServerPkt {
+    USBPacket pkt;
+
+    /* Kept for the usbredir response. */
+    struct usb_redir_control_packet_header ctrl_hdr;
+
+    /*
+     * The IOV points here. IN data lands in it, OUT data is copied in.
+     * Allocated and freed with the packet.
+     */
+    uint8_t *data;
+
+    /* Every request is on this list. */
+    QTAILQ_ENTRY(USBRedirServerPkt) inflight_link;
+
+    /* Only a request that must wait is on this list. */
+    QTAILQ_ENTRY(USBRedirServerPkt) waiting_link;
+
+    USBRedirServerPktType type;
+    USBRedirServerReply reply;
+    uint64_t redir_id;
+    int ep_idx;
+} USBRedirServerPkt;
+
+typedef struct USBRedirServerEp {
+    USBRedirServer *s;
+
+    /*
+     * Requests the device has not taken, oldest first. A new request
+     * goes on the end. We always ask about the first one, and a NAK
+     * puts it back on the front, so the order never changes.
+     */
+    QTAILQ_HEAD(, USBRedirServerPkt) waiting;
+
+    /* Asks again for a device that does not wake the endpoint. */
+    QEMUTimer *retry;
+
+    uint16_t max_packet;
+    uint8_t interval;
+    uint8_t iface;
+    uint8_t type;
+    uint8_t idx;
+} USBRedirServerEp;
+
 struct USBRedirServer {
     SysBusDevice parent_obj;
 
@@ -93,8 +180,24 @@ struct USBRedirServer {
     guint watch;
     bool host_connected;
     bool device_announced;
+
+    /* Every request the host is still waiting for an answer to. */
+    QTAILQ_HEAD(, USBRedirServerPkt) inflight;
+    uint64_t next_id;
+
+    USBRedirServerEp ep[USBREDIR_SERVER_MAX_EP];
+    uint8_t alt_setting[USB_MAX_INTERFACES];
+    GHashTable *config_table;
+    uint8_t host_config;
 };
 
+static void usbredir_server_stop_transfers(USBRedirServer *s);
+static void usbredir_server_send_cancelled(USBRedirServer *s,
+                                           USBRedirServerPkt *rp);
+static void usbredir_server_prepare_control(USBRedirServer *s,
+    USBRedirServerPkt *rp,
+    const struct usb_redir_control_packet_header *hdr);
+
 /*
  * The device is whatever USBDevice the user plugged into our port with
  * "-device <device>,bus=<id>.0". NULL until then.
@@ -104,8 +207,342 @@ static USBDevice *usbredir_server_device(USBRedirServer *s)
     return s->port.dev;
 }
 
+static USBRedirServerPkt *usbredir_server_pkt_alloc(int size, int ep_idx)
+{
+    USBRedirServerPkt *rp = g_new0(USBRedirServerPkt, 1);
+
+    qemu_iovec_init(&rp->pkt.iov, 1);
+    rp->data = g_malloc0(size);
+    rp->ep_idx = ep_idx;
+    return rp;
+}
+
+/*
+ * Take @rp off every list and free it. Do not use usb_packet_complete():
+ * it calls back into usbredir_server_packet_complete(), which would free
+ * the request a second time. usb_cancel_packet() only tells the device
+ * to let go.
+ */
+static void usbredir_server_pkt_free(USBRedirServer *s, USBRedirServerPkt *rp)
+{
+    USBRedirServerEp *ep = &s->ep[rp->ep_idx];
+
+    if (QTAILQ_IN_USE(rp, waiting_link)) {
+        QTAILQ_REMOVE(&ep->waiting, rp, waiting_link);
+    }
+    if (QTAILQ_IN_USE(rp, inflight_link)) {
+        QTAILQ_REMOVE(&s->inflight, rp, inflight_link);
+    }
+    if (usb_packet_is_inflight(&rp->pkt)) {
+        usb_cancel_packet(&rp->pkt);
+    }
+    usb_packet_cleanup(&rp->pkt);
+    g_free(rp->data);
+    g_free(rp);
+}
+
+/*
+ * Interfaces and endpoints
+ */
+
+static int usbredir_server_ep_index(uint8_t addr)
+{
+    int ep_nr = addr & 0x0f;
+
+    return (addr & USB_DIR_IN) ? ep_nr + USBREDIR_SERVER_EP_IN_BASE : ep_nr;
+}
+
+static void usbredir_server_clear_ep_table(USBRedirServer *s)
+{
+    int i;
+
+    for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
+        s->ep[i].max_packet = 0;
+        s->ep[i].interval = 0;
+        s->ep[i].iface = 0;
+        s->ep[i].type = USB_ENDPOINT_XFER_INVALID;
+    }
+}
+
+static uint8_t usbredir_server_ep_status(USBRedirServer *s, uint8_t addr,
+                                         uint8_t want_type)
+{
+    bool is_ctrl = want_type == USB_ENDPOINT_XFER_CONTROL;
+    USBDevice *device = usbredir_server_device(s);
+    int idx = usbredir_server_ep_index(addr);
+    int ep_nr = addr & 0x0f;
+
+    if (!s->host_connected || !s->device_announced ||
+        !device || !device->attached) {
+        return usb_redir_ioerror;
+    }
+
+    /* Endpoint 0 carries control transfers, and nothing else. */
+    if (ep_nr == 0 && !is_ctrl) {
+        return usb_redir_inval;
+    }
+
+    /* An absent endpoint has type INVALID, which never matches. */
+    if (!is_ctrl && s->ep[idx].type != want_type) {
+        return usb_redir_inval;
+    }
+
+    return usb_redir_success;
+}
+
+static void usbredir_server_record_endpoint(USBRedirServer *s,
+                                            const USBDescriptor *desc,
+                                            uint8_t iface)
+{
+    uint8_t addr;
+    int ep_nr;
+    int idx;
+
+    addr = desc->u.endpoint.bEndpointAddress;
+    ep_nr = addr & 0x0f;
+    idx = usbredir_server_ep_index(addr);
+    if (ep_nr < 1 || idx >= USBREDIR_SERVER_MAX_EP) {
+        trace_usbredir_server_endpoint_dropped(addr);
+        return;
+    }
+
+    s->ep[idx].type = desc->u.endpoint.bmAttributes & 0x03;
+    s->ep[idx].max_packet = (desc->u.endpoint.wMaxPacketSize_hi << 8) |
+                            desc->u.endpoint.wMaxPacketSize_lo;
+    s->ep[idx].interval = desc->u.endpoint.bInterval;
+    s->ep[idx].iface = iface;
+
+    /*
+     * bInterval says how often to poll this endpoint. Pass on what the
+     * descriptor said, but never 0 for an interrupt or isochronous
+     * endpoint: redirect.c throws the whole device away when it sees 0,
+     * so store 1 instead.
+     */
+    if (s->ep[idx].interval == 0 &&
+        (s->ep[idx].type == USB_ENDPOINT_XFER_INT ||
+         s->ep[idx].type == USB_ENDPOINT_XFER_ISOC)) {
+        s->ep[idx].interval = 1;
+    }
+}
+
+static bool usbredir_server_is_valid_config(const uint8_t *data, int len)
+{
+    const USBDescriptor *conf = (const USBDescriptor *)data;
+    unsigned int interfaces = 0;
+    bool has_interface = false;
+    const USBDescriptor *desc;
+    int pos = 0;
+
+    while (pos < len) {
+        if (len - pos < 2) {
+            return false;
+        }
+        desc = (const USBDescriptor *)(data + pos);
+        if (desc->bLength < 2 || desc->bLength > (len - pos)) {
+            return false;
+        }
+
+        switch (desc->bDescriptorType) {
+        case USB_DT_CONFIG:
+            if (pos != 0 ||
+                desc->bLength != USBREDIR_SERVER_DESC_CONFIG_SIZE) {
+                return false;
+            }
+            break;
+        case USB_DT_INTERFACE:
+            if (desc->bLength < USBREDIR_SERVER_DESC_INTERFACE_SIZE) {
+                return false;
+            }
+            if (desc->u.interface.bInterfaceNumber >= USB_MAX_INTERFACES) {
+                return false;
+            }
+            has_interface = true;
+
+            /*
+             * An interface with four alternate settings appears four
+             * times, but it is still one interface. Setting 0 is the
+             * one every interface has, so count there.
+             */
+            if (desc->u.interface.bAlternateSetting == 0) {
+                interfaces++;
+            }
+            break;
+        case USB_DT_ENDPOINT:
+            if (desc->bLength < USBREDIR_SERVER_DESC_ENDPOINT_SIZE) {
+                return false;
+            }
+            /* An endpoint belongs to the interface above it. */
+            if (!has_interface) {
+                return false;
+            }
+            break;
+        default:
+            break;
+        }
+
+        pos += desc->bLength;
+    }
+
+    return interfaces > 0 && interfaces == conf->u.config.bNumInterfaces;
+}
+
+static bool usbredir_server_save_config(USBRedirServer *s, const uint8_t *data,
+                                        int len)
+{
+    const USBDescriptor *conf = (const USBDescriptor *)data;
+    uint16_t total;
+
+    if (len < 4) {
+        return false;
+    }
+    total = lduw_le_p(data + 2);
+
+    /*
+     * The host reads this twice. The first read is a few bytes, just to
+     * learn the length from wTotalLength. The second read has the whole
+     * descriptor.
+     *
+     * A configuration descriptor is 9 bytes, so a shorter one is broken.
+     */
+    if (total < USBREDIR_SERVER_DESC_CONFIG_SIZE || len < total) {
+        return false;
+    }
+
+    if (!usbredir_server_is_valid_config(data, total)) {
+        return false;
+    }
+
+    g_hash_table_insert(s->config_table,
+                        GUINT_TO_POINTER(conf->u.config.bConfigurationValue),
+                        g_memdup2(data, total));
+
+    return true;
+}
+
+/*
+ * Some devices only forward transfers. They never read their own
+ * descriptor, so the USB core leaves their endpoint types at
+ * USB_ENDPOINT_XFER_INVALID. Sending that in ep_info does not work: the
+ * host sees type 255 and will not move data. So read the configuration
+ * descriptor and fill the tables here.
+ *
+ * Only what the host selected counts. A device can offer several
+ * configurations, and an interface several alternate settings. The
+ * endpoints of the ones not chosen are not there.
+ */
+static void usbredir_server_scan_config(USBRedirServer *s, uint8_t value,
+    struct usb_redir_interface_info_header *info)
+{
+    const USBDescriptor *desc;
+    const uint8_t *config;
+    bool active = false;
+    uint8_t iface = 0;
+    uint32_t n = 0;
+    uint8_t alt;
+    int i = 0;
+    int total;
+
+    usbredir_server_clear_ep_table(s);
+
+    config = g_hash_table_lookup(s->config_table, GUINT_TO_POINTER(value));
+    if (!config) {
+        trace_usbredir_server_config_not_found(value,
+            g_hash_table_size(s->config_table));
+        return;
+    }
+    total = lduw_le_p(config + 2);
+
+    while (i + 2 <= total) {
+        desc = (const USBDescriptor *)(config + i);
+
+        if (desc->bLength < 2) {
+            trace_usbredir_server_bad_descriptor(i, desc->bLength,
+                                                 desc->bDescriptorType);
+            break;
+        }
+        if (i + desc->bLength > total) {
+            trace_usbredir_server_descriptor_overrun(i, desc->bLength, total);
+            break;
+        }
+
+        switch (desc->bDescriptorType) {
+        case USB_DT_INTERFACE:
+            if (desc->bLength < USBREDIR_SERVER_DESC_INTERFACE_SIZE) {
+                trace_usbredir_server_bad_descriptor(i, desc->bLength,
+                                                     desc->bDescriptorType);
+                break;
+            }
+            iface = desc->u.interface.bInterfaceNumber;
+            alt = desc->u.interface.bAlternateSetting;
+            active = iface < USB_MAX_INTERFACES &&
+                     alt == s->alt_setting[iface];
+
+            if (active && n < ARRAY_SIZE(info->interface)) {
+                info->interface[n] = iface;
+                info->interface_class[n] = desc->u.interface.bInterfaceClass;
+                info->interface_subclass[n] =
+                    desc->u.interface.bInterfaceSubClass;
+                info->interface_protocol[n] =
+                    desc->u.interface.bInterfaceProtocol;
+                n++;
+            }
+            break;
+        case USB_DT_ENDPOINT:
+            if (desc->bLength < USBREDIR_SERVER_DESC_ENDPOINT_SIZE) {
+                trace_usbredir_server_bad_descriptor(i, desc->bLength,
+                                                     desc->bDescriptorType);
+                break;
+            }
+            if (active) {
+                usbredir_server_record_endpoint(s, desc, iface);
+            }
+            break;
+        }
+
+        i += desc->bLength;
+    }
+
+    info->interface_count = n;
+}
+
+static void usbredir_server_send_ep_iface_info(USBRedirServer *s,
+                                               uint8_t value)
+{
+    struct usb_redir_interface_info_header iface_info = {};
+    struct usb_redir_ep_info_header ep_info = {};
+    int i;
+
+    usbredir_server_scan_config(s, value, &iface_info);
+    trace_usbredir_server_interface_info(iface_info.interface_count);
+    usbredirparser_send_interface_info(s->parser, &iface_info);
+
+    for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
+        ep_info.type[i] = s->ep[i].type;
+        ep_info.max_packet_size[i] = s->ep[i].max_packet;
+        ep_info.interface[i] = s->ep[i].iface;
+        ep_info.interval[i] = s->ep[i].interval;
+        if (s->ep[i].type != USB_ENDPOINT_XFER_INVALID) {
+            trace_usbredir_server_ep_info(i, ep_info.type[i],
+                                          ep_info.max_packet_size[i],
+                                          ep_info.interval[i],
+                                          ep_info.interface[i]);
+        }
+    }
+
+    usbredirparser_send_ep_info(s->parser, &ep_info);
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_clear_descriptor(USBRedirServer *s)
+{
+    s->host_config = 0;
+    memset(s->alt_setting, 0, sizeof(s->alt_setting));
+    usbredir_server_clear_ep_table(s);
+    g_hash_table_remove_all(s->config_table);
+}
+
 /*
- * Device announcement
+ * Announcing the device
  */
 
 static uint8_t usbredir_server_speed(USBDevice *device)
@@ -137,6 +574,105 @@ static void usbredir_server_announce_bh(void *opaque)
     usbredirparser_do_write(s->parser);
 }
 
+/*
+ * Packet completion
+ */
+
+static uint8_t usbredir_server_status(int status)
+{
+    switch (status) {
+    case USB_RET_SUCCESS:
+        return usb_redir_success;
+    case USB_RET_STALL:
+        return usb_redir_stall;
+    case USB_RET_BABBLE:
+        return usb_redir_babble;
+    default:
+        return usb_redir_ioerror;
+    }
+}
+
+static void usbredir_server_ctrl_reply(USBRedirServer *s, uint64_t id,
+    const struct usb_redir_control_packet_header *hdr,
+    USBRedirServerReply reply, uint8_t status, uint8_t *data, int actual)
+{
+    struct usb_redir_configuration_status_header cfg = {};
+    struct usb_redir_control_packet_header resp = *hdr;
+    struct usb_redir_alt_setting_status_header alt = {
+        .interface = hdr->index,
+    };
+    bool is_in = !!(hdr->requesttype & USB_DIR_IN);
+    int data_len;
+
+    switch (reply) {
+    case USBREDIR_SERVER_REPLY_CONTROL:
+        data_len = is_in ? actual : 0;
+        resp.status = status;
+        /* Both directions report how much the device moved. */
+        resp.length = actual;
+        usbredirparser_send_control_packet(s->parser, id, &resp,
+                                           data_len > 0 ? data : NULL,
+                                           data_len);
+        break;
+    case USBREDIR_SERVER_REPLY_CONFIG:
+        cfg.status = status;
+        /* IN reports what the device sent, OUT echoes what the host chose. */
+        cfg.configuration = is_in ? (actual > 0 ? data[0] : 0) : hdr->value;
+        usbredirparser_send_configuration_status(s->parser, id, &cfg);
+        break;
+    case USBREDIR_SERVER_REPLY_ALT:
+        alt.status = status;
+        /* IN reports what the device sent, OUT echoes what the host chose. */
+        alt.alt = is_in ? (actual > 0 ? data[0] : 0) : hdr->value;
+        usbredirparser_send_alt_setting_status(s->parser, id, &alt);
+        break;
+    }
+    usbredirparser_do_write(s->parser);
+}
+
+/*
+ * The device answered the control transfer. Some requests change the
+ * endpoint list, so save what the host asked for.
+ */
+static void usbredir_server_ctrl_complete(USBRedirServer *s,
+                                          USBRedirServerPkt *rp)
+{
+    uint8_t status = usbredir_server_status(rp->pkt.status);
+    int actual = rp->pkt.actual_length;
+
+    trace_usbredir_server_ctrl_complete(rp->redir_id, status, actual);
+    usbredir_server_ctrl_reply(s, rp->redir_id, &rp->ctrl_hdr, rp->reply,
+                               status, rp->data, actual);
+
+    if (status != usb_redir_success) {
+        return;
+    }
+
+    switch ((rp->ctrl_hdr.requesttype << 8) | rp->ctrl_hdr.request) {
+    case DeviceRequest | USB_REQ_GET_DESCRIPTOR:
+        /* Keep the configuration descriptor. It says what endpoints exist. */
+        if ((rp->ctrl_hdr.value >> 8) == USB_DT_CONFIG && actual > 0) {
+            usbredir_server_save_config(s, rp->data, actual);
+        }
+        break;
+    case DeviceOutRequest | USB_REQ_SET_CONFIGURATION:
+        /* A new configuration puts every interface back on alternate 0. */
+        s->host_config = rp->ctrl_hdr.value;
+        memset(s->alt_setting, 0, sizeof(s->alt_setting));
+        usbredir_server_send_ep_iface_info(s, s->host_config);
+        break;
+    case InterfaceOutRequest | USB_REQ_SET_INTERFACE:
+        if (rp->ctrl_hdr.index >= USB_MAX_INTERFACES) {
+            return;
+        }
+        s->alt_setting[rp->ctrl_hdr.index] = rp->ctrl_hdr.value;
+        usbredir_server_send_ep_iface_info(s, s->host_config);
+        break;
+    default:
+        break;
+    }
+}
+
 /*
  * USB port ops
  */
@@ -162,6 +698,7 @@ static void usbredir_server_port_detach(USBPort *port)
     trace_usbredir_server_detach(s->device_announced);
 
     qemu_bh_cancel(s->announce_bh);
+    usbredir_server_stop_transfers(s);
 
     if (s->host_connected && s->parser && s->device_announced) {
         trace_usbredir_server_disconnect();
@@ -169,6 +706,8 @@ static void usbredir_server_port_detach(USBPort *port)
         usbredirparser_do_write(s->parser);
     }
     s->device_announced = false;
+
+    usbredir_server_clear_descriptor(s);
 }
 
 static void usbredir_server_port_child_detach(USBPort *port, USBDevice *child)
@@ -181,20 +720,136 @@ static void usbredir_server_port_wakeup(USBPort *port)
     /* We do not pass remote wakeup to the host. Nothing to do. */
 }
 
+/*
+ * The core calls this for a packet the device answered with
+ * USB_RET_ASYNC. usbredir_server_ask_device() calls it for the rest.
+ */
+static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
+{
+    USBRedirServer *s = port->opaque;
+    USBRedirServerPkt *rp = container_of(p, USBRedirServerPkt, pkt);
+    USBDevice *device = usbredir_server_device(s);
+
+    /* A cancel can arrive while we answer, so it must not find this one. */
+    QTAILQ_REMOVE(&s->inflight, rp, inflight_link);
+
+    if (!s->parser || !device) {
+        usbredir_server_pkt_free(s, rp);
+        return;
+    }
+
+    switch (rp->type) {
+    case USBREDIR_SERVER_CTRL:
+        usbredir_server_ctrl_complete(s, rp);
+        break;
+    default:
+        g_assert_not_reached();
+    }
+
+    usbredir_server_pkt_free(s, rp);
+}
+
 static USBPortOps usbredir_server_port_ops = {
     .attach = usbredir_server_port_attach,
     .detach = usbredir_server_port_detach,
     .child_detach = usbredir_server_port_child_detach,
     .wakeup = usbredir_server_port_wakeup,
+    .complete = usbredir_server_packet_complete,
 };
 
 /*
  * USB bus ops
  */
 
+/*
+ * Hand one request to the device. USB_RET_ASYNC means it answers
+ * later, and the core then calls usbredir_server_packet_complete()
+ * for us. A NAK means it has nothing yet, so keep the request.
+ * Anything else is the answer, and the core calls nothing, so we call
+ * usbredir_server_packet_complete() ourselves.
+ */
+static USBRedirServerAsk usbredir_server_ask_device(USBRedirServer *s,
+                                                    USBRedirServerPkt *rp)
+{
+    USBRedirServerEp *ep = &s->ep[rp->ep_idx];
+
+    usb_handle_packet(usbredir_server_device(s), &rp->pkt);
+
+    if (rp->pkt.status == USB_RET_ASYNC) {
+        return USBREDIR_SERVER_ASK_ASYNC;
+    }
+
+    /*
+     * Keep the request and send it again when the device wakes the
+     * endpoint.
+     */
+    if (rp->pkt.status == USB_RET_NAK) {
+        trace_usbredir_server_nak_park(rp->redir_id, rp->ep_idx);
+        QTAILQ_INSERT_HEAD(&ep->waiting, rp, waiting_link);
+        timer_mod(ep->retry, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
+                  (int64_t)USBREDIR_SERVER_RETRY_MS * SCALE_MS);
+        return USBREDIR_SERVER_ASK_NAK;
+    }
+
+    usbredir_server_packet_complete(&s->port, &rp->pkt);
+    return USBREDIR_SERVER_ASK_COMPLETE;
+}
+
+/*
+ * Send everything this endpoint is holding, oldest first. The device
+ * answering NAK stops it again, and the next wakeup or the timer picks
+ * it up.
+ */
+static void usbredir_server_kick_ep(USBRedirServer *s, int idx)
+{
+    USBRedirServerEp *ep = &s->ep[idx];
+    USBRedirServerPkt *rp;
+
+    /* We are asking now, so drop the timer. A new NAK sets it again. */
+    timer_del(ep->retry);
+
+    while ((rp = QTAILQ_FIRST(&ep->waiting)) != NULL) {
+        QTAILQ_REMOVE(&ep->waiting, rp, waiting_link);
+        trace_usbredir_server_nak_retry(rp->redir_id, idx);
+        if (usbredir_server_ask_device(s, rp) == USBREDIR_SERVER_ASK_NAK) {
+            return;
+        }
+    }
+}
+
+static void usbredir_server_retry_ep(void *opaque)
+{
+    USBRedirServerEp *ep = opaque;
+
+    usbredir_server_kick_ep(ep->s, ep->idx);
+}
+
+static void usbredir_server_wakeup_ep(USBBus *bus, USBEndpoint *ep,
+                                      unsigned int stream)
+{
+    USBRedirServer *s = container_of(bus, USBRedirServer, bus);
+    uint8_t addr = ep->nr | (ep->pid == USB_TOKEN_IN ? USB_DIR_IN : 0);
+
+    usbredir_server_kick_ep(s, usbredir_server_ep_index(addr));
+}
+
 static USBBusOps usbredir_server_bus_ops = {
+    .wakeup_endpoint = usbredir_server_wakeup_ep,
 };
 
+/* An endpoint answers in order, so this waits behind anything else. */
+static void usbredir_server_submit(USBRedirServer *s, USBRedirServerPkt *rp)
+{
+    USBRedirServerEp *ep = &s->ep[rp->ep_idx];
+
+    QTAILQ_INSERT_TAIL(&s->inflight, rp, inflight_link);
+    if (!QTAILQ_EMPTY(&ep->waiting)) {
+        QTAILQ_INSERT_TAIL(&ep->waiting, rp, waiting_link);
+        return;
+    }
+    usbredir_server_ask_device(s, rp);
+}
+
 /*
  * usbredirparser I/O and logging callbacks
  */
@@ -311,9 +966,167 @@ static void usbredir_server_reset(void *priv)
     USBDevice *device = usbredir_server_device(s);
 
     trace_usbredir_server_bus_reset(device && device->attached);
+    if (!device || !device->attached) {
+        return;
+    }
+
+    usbredir_server_stop_transfers(s);
     usb_device_reset(device);
 }
 
+static void usbredir_server_prepare_control(USBRedirServer *s,
+    USBRedirServerPkt *rp,
+    const struct usb_redir_control_packet_header *hdr)
+{
+    USBDevice *device = usbredir_server_device(s);
+    USBEndpoint *ep0 = usb_ep_get(device, USB_TOKEN_SETUP, 0);
+    bool is_in = !!(hdr->requesttype & USB_DIR_IN);
+
+    usb_packet_setup(&rp->pkt, is_in ? USB_TOKEN_IN : USB_TOKEN_OUT, ep0,
+                     0, s->next_id++, false, false);
+    if (hdr->length > 0) {
+        usb_packet_addbuf(&rp->pkt, rp->data, hdr->length);
+    }
+
+    rp->pkt.parameter = (uint64_t)hdr->requesttype |
+                        (uint64_t)hdr->request << 8 |
+                        (uint64_t)hdr->value << 16 |
+                        (uint64_t)hdr->index << 32 |
+                        (uint64_t)hdr->length << 48;
+}
+
+/*
+ * Run one control transfer on the device. @reply says which message
+ * answers the host when the transfer finishes.
+ */
+static void usbredir_server_do_control(USBRedirServer *s, uint64_t id,
+    struct usb_redir_control_packet_header *hdr,
+    uint8_t *data, int data_len, USBRedirServerReply reply)
+{
+    USBRedirServerPkt *rp;
+    uint8_t status;
+    bool is_in;
+    int size;
+
+    trace_usbredir_server_control(id, hdr->requesttype, hdr->request,
+                                  hdr->value, hdr->index, hdr->length);
+
+    /* The host still waits for an answer, even when there is nobody to ask. */
+    status = usbredir_server_ep_status(s, 0, USB_ENDPOINT_XFER_CONTROL);
+    if (status != usb_redir_success) {
+        trace_usbredir_server_ctrl_refused(id, status);
+        usbredir_server_ctrl_reply(s, id, hdr, reply, status, NULL, 0);
+        return;
+    }
+
+    is_in = !!(hdr->requesttype & USB_DIR_IN);
+
+    /* Room for the data of either direction. */
+    size = MAX(hdr->length, data_len);
+
+    rp = usbredir_server_pkt_alloc(size, 0);
+    rp->redir_id = id;
+    rp->reply = reply;
+    rp->ctrl_hdr = *hdr;
+    rp->type = USBREDIR_SERVER_CTRL;
+    if (!is_in && data_len > 0) {
+        memcpy(rp->data, data, data_len);
+    }
+
+    usbredir_server_prepare_control(s, rp, hdr);
+    usbredir_server_submit(s, rp);
+}
+
+static void usbredir_server_control_packet(void *priv, uint64_t id,
+    struct usb_redir_control_packet_header *hdr,
+    uint8_t *data, int data_len)
+{
+    USBRedirServer *s = priv;
+
+    usbredir_server_do_control(s, id, hdr, data, data_len,
+                               USBREDIR_SERVER_REPLY_CONTROL);
+    usbredirparser_free_packet_data(s->parser, data);
+}
+
+static void usbredir_server_set_configuration(void *priv, uint64_t id,
+    struct usb_redir_set_configuration_header *hdr)
+{
+    struct usb_redir_control_packet_header ctrl = {
+        .endpoint = 0,
+        .request = USB_REQ_SET_CONFIGURATION,
+        /* Host to device. The zeros mean standard, to the device. */
+        .requesttype = 0x00,
+        .status = 0,
+        .value = hdr->configuration,
+        .index = 0,
+        .length = 0,
+    };
+    USBRedirServer *s = priv;
+
+    /* The host gets a configuration_status when the device answers. */
+    usbredir_server_do_control(s, id, &ctrl, NULL, 0,
+                               USBREDIR_SERVER_REPLY_CONFIG);
+}
+
+static void usbredir_server_get_configuration(void *priv, uint64_t id)
+{
+    struct usb_redir_control_packet_header ctrl = {
+        .endpoint = 0,
+        .request = USB_REQ_GET_CONFIGURATION,
+        /* Device to host. The zeros mean standard, to the device. */
+        .requesttype = 0x80,
+        .status = 0,
+        .value = 0,
+        .index = 0,
+        .length = 1,
+    };
+    USBRedirServer *s = priv;
+
+    /* The host gets a configuration_status when the device answers. */
+    usbredir_server_do_control(s, id, &ctrl, NULL, 0,
+                               USBREDIR_SERVER_REPLY_CONFIG);
+}
+
+static void usbredir_server_set_alt_setting(void *priv, uint64_t id,
+    struct usb_redir_set_alt_setting_header *hdr)
+{
+    struct usb_redir_control_packet_header ctrl = {
+        .endpoint = 0,
+        .request = USB_REQ_SET_INTERFACE,
+        /* Host to device. The zeros mean standard, the 1 an interface. */
+        .requesttype = 0x01,
+        .status = 0,
+        .value = hdr->alt,
+        .index = hdr->interface,
+        .length = 0,
+    };
+    USBRedirServer *s = priv;
+
+    /* The host gets an alt_setting_status when the device answers. */
+    usbredir_server_do_control(s, id, &ctrl, NULL, 0,
+                               USBREDIR_SERVER_REPLY_ALT);
+}
+
+static void usbredir_server_get_alt_setting(void *priv, uint64_t id,
+    struct usb_redir_get_alt_setting_header *hdr)
+{
+    struct usb_redir_control_packet_header ctrl = {
+        .endpoint = 0,
+        .request = USB_REQ_GET_INTERFACE,
+        /* Device to host. The zeros mean standard, the 1 an interface. */
+        .requesttype = 0x81,
+        .status = 0,
+        .value = 0,
+        .index = hdr->interface,
+        .length = 1,
+    };
+    USBRedirServer *s = priv;
+
+    /* The host gets an alt_setting_status when the device answers. */
+    usbredir_server_do_control(s, id, &ctrl, NULL, 0,
+                               USBREDIR_SERVER_REPLY_ALT);
+}
+
 static void usbredir_server_filter_reject(void *priv)
 {
     trace_usbredir_server_filter_reject();
@@ -337,6 +1150,83 @@ static void usbredir_server_interface_info(void *priv,
     /* The host should not send this to a device. Nothing to do. */
 }
 
+static void usbredir_server_cancel_data_packet(void *priv, uint64_t id)
+{
+    USBRedirServer *s = priv;
+    USBRedirServerPkt *rp;
+    int idx;
+
+    QTAILQ_FOREACH(rp, &s->inflight, inflight_link) {
+        if (rp->redir_id == id) {
+            idx = rp->ep_idx;
+
+            trace_usbredir_server_cancel(id, true);
+            usbredir_server_send_cancelled(s, rp);
+            usbredir_server_pkt_free(s, rp);
+
+            /* The rest were waiting behind this one, so send them now. */
+            usbredir_server_kick_ep(s, idx);
+            return;
+        }
+    }
+
+    /*
+     * Not on our list means we already answered it. The host uses the
+     * id to find the request, and the same id is used again later. A
+     * second answer could land on the wrong request. Send nothing.
+     */
+    trace_usbredir_server_cancel(id, false);
+}
+
+/*
+ * Cancelled and in-flight packets
+ */
+
+/*
+ * Every request must get one answer carrying its id, even an aborted one.
+ * A dropped id stays in the host's cancelled queue. The host reuses ids,
+ * so a later answer would be thrown away as a stale one.
+ */
+static void usbredir_server_send_cancelled(USBRedirServer *s,
+                                           USBRedirServerPkt *rp)
+{
+    struct usb_redir_control_packet_header ctrl;
+
+    switch (rp->type) {
+    case USBREDIR_SERVER_CTRL:
+        ctrl = rp->ctrl_hdr;
+        ctrl.status = usb_redir_cancelled;
+        ctrl.length = 0;
+        usbredirparser_send_control_packet(s->parser, rp->redir_id,
+                                           &ctrl, NULL, 0);
+        break;
+    default:
+        return;
+    }
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_stop_transfers(USBRedirServer *s)
+{
+    USBRedirServerPkt *rp;
+    int i;
+
+    for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
+        timer_del(s->ep[i].retry);
+    }
+
+    /*
+     * The host waits for an answer to each of these. Send cancelled
+     * before we free them.
+     */
+    while ((rp = QTAILQ_FIRST(&s->inflight)) != NULL) {
+        if (s->parser && s->host_connected) {
+            usbredir_server_send_cancelled(s, rp);
+        }
+        usbredir_server_pkt_free(s, rp);
+    }
+}
+
 /*
  * Parser setup and teardown
  */
@@ -359,13 +1249,19 @@ static void usbredir_server_create_parser(USBRedirServer *s)
     /* Callbacks for messages the remote host sends to us */
     s->parser->hello_func = usbredir_server_hello;
     s->parser->reset_func = usbredir_server_reset;
+    s->parser->control_packet_func = usbredir_server_control_packet;
+    s->parser->set_configuration_func = usbredir_server_set_configuration;
 
     /* The parser calls these directly, so they must not be NULL. */
+    s->parser->get_configuration_func = usbredir_server_get_configuration;
+    s->parser->set_alt_setting_func = usbredir_server_set_alt_setting;
+    s->parser->get_alt_setting_func = usbredir_server_get_alt_setting;
     s->parser->filter_reject_func = usbredir_server_filter_reject;
     s->parser->filter_filter_func = usbredir_server_filter_filter;
     s->parser->device_disconnect_ack_func =
         usbredir_server_device_disconnect_ack;
     s->parser->interface_info_func = usbredir_server_interface_info;
+    s->parser->cancel_data_packet_func = usbredir_server_cancel_data_packet;
 
     /* Capabilities: 64-bit IDs, connect_device_version, ep_info sizes */
     usbredirparser_caps_set_cap(caps, usb_redir_cap_connect_device_version);
@@ -398,6 +1294,8 @@ static void usbredir_server_destroy_parser(USBRedirServer *s)
     qemu_bh_cancel(s->announce_bh);
     g_clear_handle_id(&s->watch, g_source_remove);
 
+    usbredir_server_stop_transfers(s);
+
     if (s->parser) {
         usbredirparser_destroy(s->parser);
         s->parser = NULL;
@@ -483,6 +1381,7 @@ static void usbredir_server_chardev_event(void *opaque,
 static void usbredir_server_realize(DeviceState *dev, Error **errp)
 {
     USBRedirServer *s = USB_REDIR_SERVER(dev);
+    int i;
 
     if (!qemu_chr_fe_backend_connected(&s->cs)) {
         error_setg(errp,
@@ -490,6 +1389,18 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
         return;
     }
 
+    QTAILQ_INIT(&s->inflight);
+    for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
+        QTAILQ_INIT(&s->ep[i].waiting);
+        s->ep[i].s = s;
+        s->ep[i].idx = i;
+        s->ep[i].retry = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+                                      usbredir_server_retry_ep, &s->ep[i]);
+    }
+    s->config_table = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+                                            NULL, g_free);
+    usbredir_server_clear_ep_table(s);
+
     /* One port: usbredir carries a single device. */
     usb_bus_new(&s->bus, sizeof(s->bus), &usbredir_server_bus_ops, dev);
     s->bus.auto_hub = false;
@@ -512,10 +1423,13 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
 static void usbredir_server_unrealize(DeviceState *dev)
 {
     USBRedirServer *s = USB_REDIR_SERVER(dev);
+    int i;
 
     qemu_chr_fe_deinit(&s->cs, true);
     usbredir_server_destroy_parser(s);
 
+    g_clear_pointer(&s->config_table, g_hash_table_destroy);
+
     if (s->chardev_close_bh) {
         qemu_bh_delete(s->chardev_close_bh);
         s->chardev_close_bh = NULL;
@@ -525,6 +1439,11 @@ static void usbredir_server_unrealize(DeviceState *dev)
         qemu_bh_delete(s->announce_bh);
         s->announce_bh = NULL;
     }
+
+    for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
+        timer_free(s->ep[i].retry);
+        s->ep[i].retry = NULL;
+    }
 }
 
 static const Property usbredir_server_props[] = {
hw/usb/trace-events +12 -0
index 075a9d9eae..917881ab1e 100644
@@ -402,6 +402,18 @@ usbredir_server_log(const char *msg) "%s"
 usbredir_server_hello(const char *version) "peer is %s"
 usbredir_server_attach(void) "device attached"
 usbredir_server_detach(bool announced) "device detached, was announced %d"
+usbredir_server_interface_info(uint32_t count) "interface_info for %u interface(s)"
 usbredir_server_disconnect(void) "device_disconnect sent"
 usbredir_server_bus_reset(bool attached) "bus reset, attached %d"
 usbredir_server_filter_reject(void) "host rejected our device"
+usbredir_server_cancel(uint64_t id, bool found) "id %" PRIu64 " still in flight %d"
+usbredir_server_ep_info(unsigned idx, uint8_t type, uint16_t mps, uint8_t interval, uint8_t iface) "ep_info[%u] type %u mps %u interval %u iface %u"
+usbredir_server_config_not_found(uint8_t value, unsigned have) "no descriptor for configuration %u, we have %u"
+usbredir_server_bad_descriptor(int offset, uint8_t length, uint8_t type) "descriptor at byte %d is too short: bLength %u, type %u"
+usbredir_server_descriptor_overrun(int offset, uint8_t length, int total) "descriptor at byte %d runs past the end: bLength %u, total %d"
+usbredir_server_endpoint_dropped(uint8_t address) "endpoint 0x%02x does not fit our tables"
+usbredir_server_control(uint64_t id, uint8_t requesttype, uint8_t request, uint16_t value, uint16_t index, uint16_t length) "id %" PRIu64 " type 0x%02x request 0x%02x value 0x%04x index 0x%04x length %u"
+usbredir_server_nak_park(uint64_t id, int ep) "id %" PRIu64 " ep %d"
+usbredir_server_nak_retry(uint64_t id, int ep) "id %" PRIu64 " ep %d"
+usbredir_server_ctrl_complete(uint64_t id, int status, int actual) "id %" PRIu64 " status %d actual %d"
+usbredir_server_ctrl_refused(uint64_t id, int status) "id %" PRIu64 " status %d"
Signature · 1 line
2.43.0
author
#15 lore raw

PATCH v9 15/19 hw/usb/redirect-server: Send device and interface information

In reply to Jamin Lin

The device_connect and interface_info messages currently contain zeroed fields, so clients cannot apply device filters or quirks correctly.

Read the device and configuration descriptors before announcing the device, and use them to fill these messages. Update interface and endpoint information when the configuration or alternate setting changes.

Keep internal descriptor requests separate from host transfers. Retry on NAK and stop on timeout or invalid descriptors. Do not announce the device if reading its descriptors fails.

Wait like a USB host. Wait 100 ms between the attach and the reset, and 10 ms between the reset and the first request. Some firmware turns on the pull-up before it sets up endpoint 0.

---
 hw/usb/redirect-server.c | 496 +++++++++++++++++++++++++++++++++++++++
 hw/usb/trace-events      |   7 +
 2 files changed, 503 insertions(+)
hw/usb/redirect-server.c +496 -0
index 89d88d6457..914697fdea 100644
@@ -87,6 +87,28 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
 #define USBREDIR_SERVER_DESC_CONFIG_SIZE 9
 #define USBREDIR_SERVER_DESC_INTERFACE_SIZE 9
 #define USBREDIR_SERVER_DESC_ENDPOINT_SIZE 7
+#define USBREDIR_SERVER_DESC_DEVICE_SIZE 18
+
+/*
+ * How long we keep asking the device what it is. This covers every
+ * retry and every wait, so a device that never answers stops here.
+ */
+#define USBREDIR_SERVER_DESC_TIMEOUT_MS 10000
+
+/* How long to wait before asking again after the device said NAK. */
+#define USBREDIR_SERVER_DESC_RETRY_MS 10
+
+/*
+ * The USB spec gives the device an attach debounce interval of at least
+ * 100 ms. We wait this long after the attach. Then we reset the device.
+ */
+#define USBREDIR_SERVER_ATTACH_DEBOUNCE_MS 100
+
+/*
+ * The USB spec gives the device a reset recovery time of 10 ms. We wait
+ * this long after the reset. Then we ask the device for its descriptor.
+ */
+#define USBREDIR_SERVER_RESET_RECOVERY_MS 10
 
 /*
  * Ask again this often after an endpoint answered NAK. Most devices
@@ -95,8 +117,34 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
  */
 #define USBREDIR_SERVER_RETRY_MS 1000
 
+/*
+ * The steps to read the descriptors, in order:
+ *
+ * IDLE: not started yet.
+ * ATTACH_DEBOUNCE: the device is attached.
+ *                  Wait USBREDIR_SERVER_ATTACH_DEBOUNCE_MS, then reset it.
+ * RESET_RECOVERY: the device is reset. Wait USBREDIR_SERVER_RESET_RECOVERY_MS.
+ * GET_DEVICE: ask for the device descriptor.
+ * GET_CFG_HEAD: ask for the first part of the config descriptor.
+ *               It gives the full length.
+ * GET_CFG_FULL: ask for the full config descriptor.
+ * DONE: all read. Now we can announce the device.
+ * FAILED: the read failed. Close the connection.
+ */
+typedef enum {
+    USBREDIR_SERVER_DESC_IDLE,
+    USBREDIR_SERVER_DESC_ATTACH_DEBOUNCE,
+    USBREDIR_SERVER_DESC_RESET_RECOVERY,
+    USBREDIR_SERVER_DESC_GET_DEVICE,
+    USBREDIR_SERVER_DESC_GET_CFG_HEAD,
+    USBREDIR_SERVER_DESC_GET_CFG_FULL,
+    USBREDIR_SERVER_DESC_DONE,
+    USBREDIR_SERVER_DESC_FAILED,
+} USBRedirServerDescStep;
+
 typedef enum {
     USBREDIR_SERVER_CTRL,
+    USBREDIR_SERVER_DESCRIPTOR,
 } USBRedirServerPktType;
 
 /* Which message answers the host when a control transfer ends. */
@@ -181,6 +229,22 @@ struct USBRedirServer {
     bool host_connected;
     bool device_announced;
 
+    uint8_t device_desc[USBREDIR_SERVER_DESC_DEVICE_SIZE];
+    uint8_t config_desc[USBREDIR_SERVER_DESC_CONFIG_SIZE];
+    uint8_t internal_config;
+
+    /*
+     * Our own request on endpoint 0. It never goes on inflight or
+     * waiting, so a cancel from the host cannot reach it.
+     */
+    USBRedirServerDescStep desc_step;
+    USBRedirServerPkt *desc_pkt;
+    QEMUTimer *desc_timeout;
+    QEMUTimer *desc_wait;
+    bool desc_retrying;
+    bool desc_answered;
+    QEMUBH *desc_bh;
+
     /* Every request the host is still waiting for an answer to. */
     QTAILQ_HEAD(, USBRedirServerPkt) inflight;
     uint64_t next_id;
@@ -533,14 +597,333 @@ static void usbredir_server_send_ep_iface_info(USBRedirServer *s,
     usbredirparser_do_write(s->parser);
 }
 
+/*
+ * Reading the descriptor
+ *
+ * The host cannot tell us what the device is, so we ask the device
+ * ourselves before we announce it.
+ */
+
+static const char *usbredir_server_desc_step_name(USBRedirServerDescStep step)
+{
+    static const char *const name[] = {
+        [USBREDIR_SERVER_DESC_IDLE] = "idle",
+        [USBREDIR_SERVER_DESC_ATTACH_DEBOUNCE] = "attach-debounce",
+        [USBREDIR_SERVER_DESC_RESET_RECOVERY] = "reset-recovery",
+        [USBREDIR_SERVER_DESC_GET_DEVICE] = "get-device",
+        [USBREDIR_SERVER_DESC_GET_CFG_HEAD] = "get-cfg-head",
+        [USBREDIR_SERVER_DESC_GET_CFG_FULL] = "get-cfg-full",
+        [USBREDIR_SERVER_DESC_DONE] = "done",
+        [USBREDIR_SERVER_DESC_FAILED] = "failed",
+    };
+
+    assert(step < ARRAY_SIZE(name) && name[step]);
+    return name[step];
+}
+
+static void usbredir_server_cancel_descriptor(USBRedirServer *s)
+{
+    USBRedirServerPkt *rp = s->desc_pkt;
+
+    qemu_bh_cancel(s->desc_bh);
+    timer_del(s->desc_wait);
+    timer_del(s->desc_timeout);
+    s->desc_pkt = NULL;
+    s->desc_retrying = false;
+    s->desc_answered = false;
+    if (rp) {
+        usbredir_server_pkt_free(s, rp);
+    }
+}
+
 static void usbredir_server_clear_descriptor(USBRedirServer *s)
 {
+    usbredir_server_cancel_descriptor(s);
+    s->desc_step = USBREDIR_SERVER_DESC_IDLE;
+    s->internal_config = 0;
     s->host_config = 0;
+    memset(s->device_desc, 0, sizeof(s->device_desc));
+    memset(s->config_desc, 0, sizeof(s->config_desc));
     memset(s->alt_setting, 0, sizeof(s->alt_setting));
     usbredir_server_clear_ep_table(s);
     g_hash_table_remove_all(s->config_table);
 }
 
+static void usbredir_server_descriptor_failed(USBRedirServer *s,
+                                              const char *reason)
+{
+    error_report(TYPE_USB_REDIR_SERVER ": could not read the descriptor "
+                 "at step %s: %s",
+                 usbredir_server_desc_step_name(s->desc_step), reason);
+    usbredir_server_clear_descriptor(s);
+    s->desc_step = USBREDIR_SERVER_DESC_FAILED;
+    qemu_bh_schedule(s->announce_bh);
+}
+
+static void usbredir_server_descriptor_timeout(void *opaque)
+{
+    USBRedirServer *s = opaque;
+
+    usbredir_server_descriptor_failed(s, "the device took too long");
+}
+
+static void usbredir_server_descriptor_timer(void *opaque)
+{
+    USBRedirServer *s = opaque;
+
+    qemu_bh_schedule(s->desc_bh);
+}
+
+static void usbredir_server_submit_descriptor(USBRedirServer *s)
+{
+    const char *step = usbredir_server_desc_step_name(s->desc_step);
+    USBRedirServerPkt *rp = s->desc_pkt;
+
+    s->desc_retrying = false;
+    timer_del(s->desc_wait);
+
+    /* The same packet goes out again after a NAK, so empty it first. */
+    rp->pkt.actual_length = 0;
+    memset(rp->data, 0, rp->pkt.iov.size);
+
+    trace_usbredir_server_submit_descriptor(step);
+    usb_handle_packet(usbredir_server_device(s), &rp->pkt);
+
+    switch (rp->pkt.status) {
+    case USB_RET_ASYNC:
+        /*
+         * The device keeps the packet and its buffer. It answers
+         * later, and the core calls usbredir_server_packet_complete().
+         */
+        break;
+    case USB_RET_NAK:
+        s->desc_retrying = true;
+        timer_mod(s->desc_wait, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
+                  (int64_t)USBREDIR_SERVER_DESC_RETRY_MS * SCALE_MS);
+        break;
+    default:
+        /*
+         * The transfer ended. SUCCESS has the data, STALL and the
+         * other errors do not. The bottom half looks at the status
+         * and decides which.
+         */
+        s->desc_answered = true;
+        qemu_bh_schedule(s->desc_bh);
+        break;
+    }
+}
+
+static void usbredir_server_ask_descriptor(USBRedirServer *s, uint8_t type,
+                                           uint16_t length)
+{
+    struct usb_redir_control_packet_header hdr = {
+        .endpoint = 0,
+        .request = USB_REQ_GET_DESCRIPTOR,
+        /* Device to host. The zeros mean standard, to the device. */
+        .requesttype = USB_DIR_IN,
+        .status = 0,
+        .value = type << 8,
+        .index = 0,
+        .length = length,
+    };
+    USBRedirServerPkt *rp;
+
+    assert(!s->desc_pkt);
+    trace_usbredir_server_ask_descriptor(type, length);
+
+    rp = usbredir_server_pkt_alloc(length, 0);
+    rp->type = USBREDIR_SERVER_DESCRIPTOR;
+    usbredir_server_prepare_control(s, rp, &hdr);
+    s->desc_pkt = rp;
+    usbredir_server_submit_descriptor(s);
+}
+
+static bool usbredir_server_accept_descriptor(USBRedirServer *s,
+                                              const uint8_t *data,
+                                              int actual_length)
+{
+    const char *step = usbredir_server_desc_step_name(s->desc_step);
+    const USBDescriptor *desc = (const USBDescriptor *)data;
+    const USBDescriptor *head;
+    uint16_t total;
+
+    switch (s->desc_step) {
+    case USBREDIR_SERVER_DESC_GET_DEVICE:
+        if (actual_length != USBREDIR_SERVER_DESC_DEVICE_SIZE ||
+            desc->bLength != USBREDIR_SERVER_DESC_DEVICE_SIZE ||
+            desc->bDescriptorType != USB_DT_DEVICE) {
+            trace_usbredir_server_accept_descriptor(step,
+                                                    "not a device descriptor");
+            return false;
+        }
+        if (desc->u.device.bNumConfigurations == 0) {
+            trace_usbredir_server_accept_descriptor(step,
+                                                    "no configuration");
+            return false;
+        }
+        memcpy(s->device_desc, data, actual_length);
+        s->desc_step = USBREDIR_SERVER_DESC_GET_CFG_HEAD;
+        break;
+    case USBREDIR_SERVER_DESC_GET_CFG_HEAD:
+        if (actual_length != USBREDIR_SERVER_DESC_CONFIG_SIZE ||
+            desc->bLength != USBREDIR_SERVER_DESC_CONFIG_SIZE ||
+            desc->bDescriptorType != USB_DT_CONFIG) {
+            trace_usbredir_server_accept_descriptor(step,
+                "not a configuration descriptor");
+            return false;
+        }
+        if (desc->u.config.bConfigurationValue == 0) {
+            trace_usbredir_server_accept_descriptor(step,
+                                                    "configuration value 0");
+            return false;
+        }
+        total = desc->u.config.wTotalLength_hi << 8 |
+                desc->u.config.wTotalLength_lo;
+        if (total < USBREDIR_SERVER_DESC_CONFIG_SIZE ||
+            total > USB_DESC_MAX_LEN) {
+            trace_usbredir_server_desc_too_long(total);
+            return false;
+        }
+        memcpy(s->config_desc, data, sizeof(s->config_desc));
+        s->desc_step = USBREDIR_SERVER_DESC_GET_CFG_FULL;
+        break;
+    case USBREDIR_SERVER_DESC_GET_CFG_FULL:
+        head = (const USBDescriptor *)s->config_desc;
+        total = head->u.config.wTotalLength_hi << 8 |
+                head->u.config.wTotalLength_lo;
+        if (actual_length != total) {
+            trace_usbredir_server_accept_descriptor(step,
+                                                    "wrong length");
+            return false;
+        }
+        if (memcmp(data, s->config_desc, sizeof(s->config_desc))) {
+            trace_usbredir_server_accept_descriptor(step,
+                                                    "the head changed");
+            return false;
+        }
+        if (!usbredir_server_save_config(s, data, actual_length)) {
+            trace_usbredir_server_accept_descriptor(step,
+                "the configuration does not hold together");
+            return false;
+        }
+        s->internal_config = desc->u.config.bConfigurationValue;
+        s->desc_step = USBREDIR_SERVER_DESC_DONE;
+        break;
+    default:
+        g_assert_not_reached();
+    }
+
+    return true;
+}
+
+static void usbredir_server_descriptor_answer(USBRedirServer *s,
+                                              USBRedirServerPkt *rp)
+{
+    const char *step = usbredir_server_desc_step_name(s->desc_step);
+
+    trace_usbredir_server_descriptor_answer(step, rp->pkt.status,
+                                            rp->pkt.actual_length);
+
+    if (rp->pkt.status != USB_RET_SUCCESS) {
+        usbredir_server_descriptor_failed(s, "the transfer failed");
+        return;
+    }
+
+    if (!usbredir_server_accept_descriptor(s, rp->data,
+                                           rp->pkt.actual_length)) {
+        usbredir_server_descriptor_failed(s,
+            "the answer is not what we asked for");
+        return;
+    }
+
+    /*
+     * usbredir_server_accept_descriptor() set the next step. DONE
+     * means the read is finished.
+     */
+    if (s->desc_step == USBREDIR_SERVER_DESC_DONE) {
+        timer_del(s->desc_timeout);
+        qemu_bh_schedule(s->announce_bh);
+        return;
+    }
+
+    qemu_bh_schedule(s->desc_bh);
+}
+
+static void usbredir_server_descriptor_bh(void *opaque)
+{
+    USBRedirServer *s = opaque;
+    USBDevice *device = usbredir_server_device(s);
+    USBRedirServerPkt *rp = s->desc_pkt;
+
+    if (!s->host_connected || !s->parser || !device || !device->attached) {
+        return;
+    }
+
+    switch (s->desc_step) {
+    case USBREDIR_SERVER_DESC_ATTACH_DEBOUNCE:
+        /*
+         * After attach the device is in ATTACHED state, and
+         * usb_handle_packet() only takes a packet in DEFAULT state.
+         * A reset moves it there.
+         */
+        usb_device_reset(device);
+        s->desc_step = USBREDIR_SERVER_DESC_RESET_RECOVERY;
+        timer_mod(s->desc_wait, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
+                  (int64_t)USBREDIR_SERVER_RESET_RECOVERY_MS * SCALE_MS);
+        return;
+    case USBREDIR_SERVER_DESC_RESET_RECOVERY:
+        s->desc_step = USBREDIR_SERVER_DESC_GET_DEVICE;
+        qemu_bh_schedule(s->desc_bh);
+        return;
+    case USBREDIR_SERVER_DESC_GET_DEVICE:
+        if (!rp) {
+            usbredir_server_ask_descriptor(s, USB_DT_DEVICE,
+                                           USBREDIR_SERVER_DESC_DEVICE_SIZE);
+            return;
+        }
+        break;
+    case USBREDIR_SERVER_DESC_GET_CFG_HEAD:
+        if (!rp) {
+            usbredir_server_ask_descriptor(s, USB_DT_CONFIG,
+                                           USBREDIR_SERVER_DESC_CONFIG_SIZE);
+            return;
+        }
+        break;
+    case USBREDIR_SERVER_DESC_GET_CFG_FULL:
+        if (!rp) {
+            usbredir_server_ask_descriptor(s, USB_DT_CONFIG,
+                                           lduw_le_p(s->config_desc + 2));
+            return;
+        }
+        break;
+    default:
+        /* IDLE, DONE and FAILED have nothing to send. */
+        return;
+    }
+
+    /* rp is the request we gave the device. */
+    if (s->desc_answered) {
+        /*
+         * Set s->desc_pkt to NULL first. On failure,
+         * usbredir_server_descriptor_answer() ends up in
+         * usbredir_server_cancel_descriptor(), which frees s->desc_pkt.
+         * With NULL there, only the line below frees the packet.
+         */
+        s->desc_pkt = NULL;
+        s->desc_answered = false;
+        usbredir_server_descriptor_answer(s, rp);
+        usbredir_server_pkt_free(s, rp);
+        return;
+    }
+
+    if (s->desc_retrying) {
+        usbredir_server_submit_descriptor(s);
+        return;
+    }
+
+    /* The device is still holding the request. */
+}
+
 /*
  * Announcing the device
  */
@@ -557,6 +940,22 @@ static uint8_t usbredir_server_speed(USBDevice *device)
     }
 }
 
+static void usbredir_server_describe_device(USBRedirServer *s,
+    struct usb_redir_device_connect_header *conn)
+{
+    const USBDescriptor *desc = (const USBDescriptor *)s->device_desc;
+
+    conn->device_class = desc->u.device.bDeviceClass;
+    conn->device_subclass = desc->u.device.bDeviceSubClass;
+    conn->device_protocol = desc->u.device.bDeviceProtocol;
+    conn->vendor_id = desc->u.device.idVendor_hi << 8 |
+                      desc->u.device.idVendor_lo;
+    conn->product_id = desc->u.device.idProduct_hi << 8 |
+                       desc->u.device.idProduct_lo;
+    conn->device_version_bcd = desc->u.device.bcdDevice_hi << 8 |
+                               desc->u.device.bcdDevice_lo;
+}
+
 static void usbredir_server_announce_bh(void *opaque)
 {
     USBRedirServer *s = opaque;
@@ -567,9 +966,42 @@ static void usbredir_server_announce_bh(void *opaque)
         !device || !device->attached) {
         return;
     }
+
+    if (s->desc_step == USBREDIR_SERVER_DESC_IDLE) {
+        s->desc_step = USBREDIR_SERVER_DESC_ATTACH_DEBOUNCE;
+        timer_mod(s->desc_timeout, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
+                  (int64_t)USBREDIR_SERVER_DESC_TIMEOUT_MS * SCALE_MS);
+        timer_mod(s->desc_wait, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
+                  (int64_t)USBREDIR_SERVER_ATTACH_DEBOUNCE_MS * SCALE_MS);
+        return;
+    }
+
+    /*
+     * We could not read the descriptor. The host waits for
+     * device_connect, so close the connection to tell it.
+     */
+    if (s->desc_step == USBREDIR_SERVER_DESC_FAILED) {
+        trace_usbredir_server_announce_failed();
+        qemu_chr_fe_disconnect(&s->cs);
+        return;
+    }
+
+    if (s->desc_step != USBREDIR_SERVER_DESC_DONE) {
+        return;
+    }
+
     s->device_announced = true;
     conn.speed = usbredir_server_speed(device);
+    usbredir_server_describe_device(s, &conn);
 
+    /*
+     * Send this first. The host looks the device over when
+     * device_connect arrives, and this is what it looks at.
+     */
+    usbredir_server_send_ep_iface_info(s, s->internal_config);
+
+    trace_usbredir_server_announce(conn.speed, conn.vendor_id,
+                                   conn.product_id, conn.device_class);
     usbredirparser_send_device_connect(s->parser, &conn);
     usbredirparser_do_write(s->parser);
 }
@@ -730,6 +1162,28 @@ static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
     USBRedirServerPkt *rp = container_of(p, USBRedirServerPkt, pkt);
     USBDevice *device = usbredir_server_device(s);
 
+    /*
+     * Our own request. We read the descriptor before we announce
+     * the device, so the host knows what it is getting.
+     */
+    if (rp->type == USBREDIR_SERVER_DESCRIPTOR) {
+        assert(s->desc_pkt == rp);
+
+        /*
+         * If the endpoint halts with this packet still queued, the core
+         * leaves it there and loops in usb_packet_complete() until the
+         * queue is empty. A bottom half would never run, so take it off
+         * now.
+         */
+        if (p->status == USB_RET_REMOVE_FROM_QUEUE &&
+            usb_packet_is_inflight(p)) {
+            usb_cancel_packet(p);
+        }
+        s->desc_answered = true;
+        qemu_bh_schedule(s->desc_bh);
+        return;
+    }
+
     /* A cancel can arrive while we answer, so it must not find this one. */
     QTAILQ_REMOVE(&s->inflight, rp, inflight_link);
 
@@ -830,6 +1284,16 @@ static void usbredir_server_wakeup_ep(USBBus *bus, USBEndpoint *ep,
     USBRedirServer *s = container_of(bus, USBRedirServer, bus);
     uint8_t addr = ep->nr | (ep->pid == USB_TOKEN_IN ? USB_DIR_IN : 0);
 
+    /*
+     * Our descriptor request is not on inflight or waiting, so
+     * usbredir_server_kick_ep() below cannot see it. The device is
+     * still running when it calls us, so sending now would call back
+     * into it.
+     */
+    if (ep->nr == 0 && s->desc_retrying) {
+        qemu_bh_schedule(s->desc_bh);
+    }
+
     usbredir_server_kick_ep(s, usbredir_server_ep_index(addr));
 }
 
@@ -972,6 +1436,16 @@ static void usbredir_server_reset(void *priv)
 
     usbredir_server_stop_transfers(s);
     usb_device_reset(device);
+
+    /*
+     * Before the announcement, usbredir_server_stop_transfers() above
+     * also stopped our descriptor read. Start it again from the
+     * beginning.
+     */
+    if (!s->device_announced) {
+        usbredir_server_clear_descriptor(s);
+        qemu_bh_schedule(s->announce_bh);
+    }
 }
 
 static void usbredir_server_prepare_control(USBRedirServer *s,
@@ -1211,6 +1685,8 @@ static void usbredir_server_stop_transfers(USBRedirServer *s)
     USBRedirServerPkt *rp;
     int i;
 
+    usbredir_server_cancel_descriptor(s);
+
     for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
         timer_del(s->ep[i].retry);
     }
@@ -1291,6 +1767,7 @@ static void usbredir_server_destroy_parser(USBRedirServer *s)
     s->host_connected = false;
     s->device_announced = false;
 
+    usbredir_server_clear_descriptor(s);
     qemu_bh_cancel(s->announce_bh);
     g_clear_handle_id(&s->watch, g_source_remove);
 
@@ -1364,6 +1841,9 @@ static void usbredir_server_chardev_event(void *opaque,
         break;
     case CHR_EVENT_CLOSED:
         trace_usbredir_server_chardev_close();
+        s->host_connected = false;
+        qemu_bh_cancel(s->announce_bh);
+        usbredir_server_cancel_descriptor(s);
         qemu_bh_schedule(s->chardev_close_bh);
         break;
     case CHR_EVENT_BREAK:
@@ -1412,6 +1892,12 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
                                               s, &dev->mem_reentrancy_guard);
     s->announce_bh = qemu_bh_new_guarded(usbredir_server_announce_bh, s,
                                          &dev->mem_reentrancy_guard);
+    s->desc_bh = qemu_bh_new_guarded(usbredir_server_descriptor_bh, s,
+                                     &dev->mem_reentrancy_guard);
+    s->desc_wait = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+                                usbredir_server_descriptor_timer, s);
+    s->desc_timeout = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+                                   usbredir_server_descriptor_timeout, s);
 
     qemu_chr_fe_set_handlers(&s->cs,
                              usbredir_server_chardev_can_read,
@@ -1440,6 +1926,16 @@ static void usbredir_server_unrealize(DeviceState *dev)
         s->announce_bh = NULL;
     }
 
+    if (s->desc_bh) {
+        qemu_bh_delete(s->desc_bh);
+        s->desc_bh = NULL;
+    }
+
+    timer_free(s->desc_wait);
+    s->desc_wait = NULL;
+    timer_free(s->desc_timeout);
+    s->desc_timeout = NULL;
+
     for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
         timer_free(s->ep[i].retry);
         s->ep[i].retry = NULL;
hw/usb/trace-events +7 -0
index 917881ab1e..549f5f6e23 100644
@@ -402,7 +402,14 @@ usbredir_server_log(const char *msg) "%s"
 usbredir_server_hello(const char *version) "peer is %s"
 usbredir_server_attach(void) "device attached"
 usbredir_server_detach(bool announced) "device detached, was announced %d"
+usbredir_server_announce(uint8_t speed, uint16_t vendor, uint16_t product, uint8_t class) "device_connect speed %u, %04x:%04x, class %u"
+usbredir_server_announce_failed(void) "closing the connection"
+usbredir_server_ask_descriptor(uint8_t type, uint16_t length) "asking the device for descriptor type %u, %u bytes"
+usbredir_server_submit_descriptor(const char *step) "step %s"
+usbredir_server_descriptor_answer(const char *step, int status, int length) "step %s status %d length %d"
+usbredir_server_accept_descriptor(const char *step, const char *reason) "step %s rejected: %s"
 usbredir_server_interface_info(uint32_t count) "interface_info for %u interface(s)"
+usbredir_server_desc_too_long(uint16_t total) "configuration descriptor of %u bytes is more than we take"
 usbredir_server_disconnect(void) "device_disconnect sent"
 usbredir_server_bus_reset(bool attached) "bus reset, attached %d"
 usbredir_server_filter_reject(void) "host rejected our device"
Signature · 1 line
2.43.0
author
#16 lore raw

PATCH v9 16/19 hw/usb/redirect-server: Implement bulk and interrupt transfers

In reply to Jamin Lin

Send bulk and interrupt packets to the device and return the result to the host. Each one is a request and an answer, so they reuse the packet tracking and the NAK handling added for control transfers.

A bulk request carries a 32 bit length, so the host can ask us to allocate up to 4 GB. Refuse a request that big. Do not answer it with fewer bytes instead: the host counts the bytes it gets back, and it resets the device when some are missing.

Advertise usb_redir_cap_32bits_bulk_length. We already read and write the high half of the length, but without the capability redirect.c asserts when its own guest asks for a transfer larger than 64KiB.

---
 hw/usb/redirect-server.c | 240 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |   6 +
 2 files changed, 245 insertions(+), 1 deletion(-)
hw/usb/redirect-server.c +239 -1
index 914697fdea..dce92eb263 100644
@@ -117,6 +117,12 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
  */
 #define USBREDIR_SERVER_RETRY_MS 1000
 
+/*
+ * The bulk length field is 32 bits, so the host can ask for up to 4 GB.
+ * This is the largest transfer accepted.
+ */
+#define USBREDIR_SERVER_MAX_BULK (1 * MiB)
+
 /*
  * The steps to read the descriptors, in order:
  *
@@ -145,6 +151,8 @@ typedef enum {
 typedef enum {
     USBREDIR_SERVER_CTRL,
     USBREDIR_SERVER_DESCRIPTOR,
+    USBREDIR_SERVER_BULK,
+    USBREDIR_SERVER_INTERRUPT,
 } USBRedirServerPktType;
 
 /* Which message answers the host when a control transfer ends. */
@@ -168,7 +176,9 @@ typedef struct USBRedirServerPkt {
     USBPacket pkt;
 
     /* Kept for the usbredir response. */
+    struct usb_redir_interrupt_packet_header intr_hdr;
     struct usb_redir_control_packet_header ctrl_hdr;
+    struct usb_redir_bulk_packet_header bulk_hdr;
 
     /*
      * The IOV points here. IN data lands in it, OUT data is copied in.
@@ -1105,6 +1115,67 @@ static void usbredir_server_ctrl_complete(USBRedirServer *s,
     }
 }
 
+static void usbredir_server_bulk_reply(USBRedirServer *s, uint64_t id,
+    const struct usb_redir_bulk_packet_header *hdr, uint8_t status,
+    uint8_t *data, int actual)
+{
+    struct usb_redir_bulk_packet_header resp = *hdr;
+    bool is_in = !!(hdr->endpoint & USB_DIR_IN);
+    int data_len = is_in ? actual : 0;
+
+    resp.status = status;
+    /* Both directions report how much the device moved. */
+    resp.length = actual;
+    resp.length_high = actual >> 16;
+
+    usbredirparser_send_bulk_packet(s->parser, id, &resp,
+                                    data_len > 0 ? data : NULL, data_len);
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_bulk_complete(USBRedirServer *s,
+                                          USBRedirServerPkt *rp)
+{
+    uint8_t status = usbredir_server_status(rp->pkt.status);
+    int actual = rp->pkt.actual_length;
+
+    trace_usbredir_server_bulk_complete(rp->redir_id, rp->bulk_hdr.endpoint,
+                                        status, actual);
+    usbredir_server_bulk_reply(s, rp->redir_id, &rp->bulk_hdr, status,
+                               rp->data, actual);
+}
+
+static void usbredir_server_interrupt_reply(USBRedirServer *s, uint64_t id,
+    const struct usb_redir_interrupt_packet_header *hdr, uint8_t status,
+    uint8_t *data, int actual)
+{
+    struct usb_redir_interrupt_packet_header resp = *hdr;
+    bool is_in = !!(hdr->endpoint & USB_DIR_IN);
+    int data_len = is_in ? actual : 0;
+
+    resp.status = status;
+    /* Both directions report how much the device moved. */
+    resp.length = actual;
+
+    usbredirparser_send_interrupt_packet(s->parser, id, &resp,
+                                         data_len > 0 ? data : NULL,
+                                         data_len);
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_interrupt_complete(USBRedirServer *s,
+                                               USBRedirServerPkt *rp)
+{
+    uint8_t status = usbredir_server_status(rp->pkt.status);
+    int actual = rp->pkt.actual_length;
+
+    trace_usbredir_server_interrupt_complete(rp->redir_id,
+                                             rp->intr_hdr.endpoint,
+                                             status, actual);
+    usbredir_server_interrupt_reply(s, rp->redir_id, &rp->intr_hdr, status,
+                                    rp->data, actual);
+}
+
 /*
  * USB port ops
  */
@@ -1196,6 +1267,12 @@ static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
     case USBREDIR_SERVER_CTRL:
         usbredir_server_ctrl_complete(s, rp);
         break;
+    case USBREDIR_SERVER_BULK:
+        usbredir_server_bulk_complete(s, rp);
+        break;
+    case USBREDIR_SERVER_INTERRUPT:
+        usbredir_server_interrupt_complete(s, rp);
+        break;
     default:
         g_assert_not_reached();
     }
@@ -1601,6 +1678,121 @@ static void usbredir_server_get_alt_setting(void *priv, uint64_t id,
                                USBREDIR_SERVER_REPLY_ALT);
 }
 
+static void usbredir_server_bulk_packet(void *priv, uint64_t id,
+    struct usb_redir_bulk_packet_header *hdr,
+    uint8_t *data, int data_len)
+{
+    USBRedirServer *s = priv;
+    int idx = usbredir_server_ep_index(hdr->endpoint);
+    USBDevice *device = usbredir_server_device(s);
+    bool is_in = !!(hdr->endpoint & USB_DIR_IN);
+    int pid = is_in ? USB_TOKEN_IN : USB_TOKEN_OUT;
+    int ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServerPkt *rp;
+    USBEndpoint *ep;
+    uint8_t status;
+    uint32_t len;
+
+    /* The host still waits for an answer, even to a request we refuse. */
+    status = usbredir_server_ep_status(s, hdr->endpoint,
+                                       USB_ENDPOINT_XFER_BULK);
+    if (status != usb_redir_success) {
+        trace_usbredir_server_refused(id, hdr->endpoint, status,
+                                      USB_ENDPOINT_XFER_BULK,
+                                      s->ep[idx].type);
+        usbredir_server_bulk_reply(s, id, hdr, status, NULL, 0);
+        usbredirparser_free_packet_data(s->parser, data);
+        return;
+    }
+
+    /* IN: what the host asked for. OUT: what the host sent. */
+    len = is_in ? (((uint32_t)hdr->length_high << 16) | hdr->length)
+                : (uint32_t)data_len;
+
+    /*
+     * Too big. Tell the host the transfer failed.
+     * Do not send back less data instead. The host would see the
+     * missing bytes as an error and reset the device.
+     */
+    if (len > USBREDIR_SERVER_MAX_BULK) {
+        trace_usbredir_server_bulk_too_big(id, hdr->endpoint, len);
+        usbredir_server_bulk_reply(s, id, hdr, usb_redir_inval, NULL, 0);
+        usbredirparser_free_packet_data(s->parser, data);
+        return;
+    }
+
+    ep = usb_ep_get(device, pid, ep_nr);
+    rp = usbredir_server_pkt_alloc(len, idx);
+    rp->redir_id = id;
+    rp->type = USBREDIR_SERVER_BULK;
+    rp->bulk_hdr = *hdr;
+
+    usb_packet_setup(&rp->pkt, pid, ep, 0, s->next_id++, false, false);
+
+    /* OUT data comes from the host. IN data is written by the device. */
+    if (!is_in && len > 0) {
+        memcpy(rp->data, data, len);
+    }
+    usb_packet_addbuf(&rp->pkt, rp->data, len);
+
+    trace_usbredir_server_bulk(id, hdr->endpoint, len);
+    usbredirparser_free_packet_data(s->parser, data);
+    usbredir_server_submit(s, rp);
+}
+
+/*
+ * The host sends an interrupt_packet. It is a request: interrupt OUT
+ * data to write, or a single IN request to answer.
+ */
+static void usbredir_server_interrupt_packet(void *priv, uint64_t id,
+    struct usb_redir_interrupt_packet_header *hdr,
+    uint8_t *data, int data_len)
+{
+    USBRedirServer *s = priv;
+    int idx = usbredir_server_ep_index(hdr->endpoint);
+    USBDevice *device = usbredir_server_device(s);
+    bool is_in = !!(hdr->endpoint & USB_DIR_IN);
+    int pid = is_in ? USB_TOKEN_IN : USB_TOKEN_OUT;
+    int ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServerPkt *rp;
+    USBEndpoint *ep;
+    uint8_t status;
+    int len;
+
+    /* The host still waits for an answer, even to a request we refuse. */
+    status = usbredir_server_ep_status(s, hdr->endpoint,
+                                       USB_ENDPOINT_XFER_INT);
+    if (status != usb_redir_success) {
+        trace_usbredir_server_refused(id, hdr->endpoint, status,
+                                      USB_ENDPOINT_XFER_INT,
+                                      s->ep[idx].type);
+        usbredir_server_interrupt_reply(s, id, hdr, status, NULL, 0);
+        usbredirparser_free_packet_data(s->parser, data);
+        return;
+    }
+
+    /* IN: what the host asked for. OUT: what the host sent. */
+    len = is_in ? hdr->length : data_len;
+
+    ep = usb_ep_get(device, pid, ep_nr);
+    rp = usbredir_server_pkt_alloc(len, idx);
+    rp->redir_id = id;
+    rp->type = USBREDIR_SERVER_INTERRUPT;
+    rp->intr_hdr = *hdr;
+
+    usb_packet_setup(&rp->pkt, pid, ep, 0, s->next_id++, false, false);
+
+    /* OUT data comes from the host. IN data is written by the device. */
+    if (!is_in && len > 0) {
+        memcpy(rp->data, data, len);
+    }
+    usb_packet_addbuf(&rp->pkt, rp->data, len);
+
+    trace_usbredir_server_interrupt(id, hdr->endpoint, len);
+    usbredirparser_free_packet_data(s->parser, data);
+    usbredir_server_submit(s, rp);
+}
+
 static void usbredir_server_filter_reject(void *priv)
 {
     trace_usbredir_server_filter_reject();
@@ -1624,6 +1816,24 @@ static void usbredir_server_interface_info(void *priv,
     /* The host should not send this to a device. Nothing to do. */
 }
 
+static void usbredir_server_alloc_bulk_streams(void *priv, uint64_t id,
+    struct usb_redir_alloc_bulk_streams_header *hdr)
+{
+    /* We do not advertise bulk streams. Nothing to do. */
+}
+
+static void usbredir_server_start_bulk_receiving(void *priv, uint64_t id,
+    struct usb_redir_start_bulk_receiving_header *hdr)
+{
+    /* We do not advertise this. Nothing to do. */
+}
+
+static void usbredir_server_stop_bulk_receiving(void *priv, uint64_t id,
+    struct usb_redir_stop_bulk_receiving_header *hdr)
+{
+    /* We do not advertise this. Nothing to do. */
+}
+
 static void usbredir_server_cancel_data_packet(void *priv, uint64_t id)
 {
     USBRedirServer *s = priv;
@@ -1664,7 +1874,9 @@ static void usbredir_server_cancel_data_packet(void *priv, uint64_t id)
 static void usbredir_server_send_cancelled(USBRedirServer *s,
                                            USBRedirServerPkt *rp)
 {
+    struct usb_redir_interrupt_packet_header intr;
     struct usb_redir_control_packet_header ctrl;
+    struct usb_redir_bulk_packet_header bulk;
 
     switch (rp->type) {
     case USBREDIR_SERVER_CTRL:
@@ -1674,6 +1886,21 @@ static void usbredir_server_send_cancelled(USBRedirServer *s,
         usbredirparser_send_control_packet(s->parser, rp->redir_id,
                                            &ctrl, NULL, 0);
         break;
+    case USBREDIR_SERVER_BULK:
+        bulk = rp->bulk_hdr;
+        bulk.status = usb_redir_cancelled;
+        bulk.length_high = 0;
+        bulk.length = 0;
+        usbredirparser_send_bulk_packet(s->parser, rp->redir_id,
+                                        &bulk, NULL, 0);
+        break;
+    case USBREDIR_SERVER_INTERRUPT:
+        intr = rp->intr_hdr;
+        intr.status = usb_redir_cancelled;
+        intr.length = 0;
+        usbredirparser_send_interrupt_packet(s->parser, rp->redir_id,
+                                             &intr, NULL, 0);
+        break;
     default:
         return;
     }
@@ -1726,6 +1953,8 @@ static void usbredir_server_create_parser(USBRedirServer *s)
     s->parser->hello_func = usbredir_server_hello;
     s->parser->reset_func = usbredir_server_reset;
     s->parser->control_packet_func = usbredir_server_control_packet;
+    s->parser->bulk_packet_func = usbredir_server_bulk_packet;
+    s->parser->interrupt_packet_func = usbredir_server_interrupt_packet;
     s->parser->set_configuration_func = usbredir_server_set_configuration;
 
     /* The parser calls these directly, so they must not be NULL. */
@@ -1737,12 +1966,21 @@ static void usbredir_server_create_parser(USBRedirServer *s)
     s->parser->device_disconnect_ack_func =
         usbredir_server_device_disconnect_ack;
     s->parser->interface_info_func = usbredir_server_interface_info;
+    s->parser->alloc_bulk_streams_func = usbredir_server_alloc_bulk_streams;
     s->parser->cancel_data_packet_func = usbredir_server_cancel_data_packet;
+    s->parser->start_bulk_receiving_func =
+        usbredir_server_start_bulk_receiving;
+    s->parser->stop_bulk_receiving_func =
+        usbredir_server_stop_bulk_receiving;
 
-    /* Capabilities: 64-bit IDs, connect_device_version, ep_info sizes */
+    /*
+     * Capabilities: 64-bit IDs, connect_device_version, ep_info sizes
+     * and 32-bit bulk lengths.
+     */
     usbredirparser_caps_set_cap(caps, usb_redir_cap_connect_device_version);
     usbredirparser_caps_set_cap(caps, usb_redir_cap_ep_info_max_packet_size);
     usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids);
+    usbredirparser_caps_set_cap(caps, usb_redir_cap_32bits_bulk_length);
 
     /*
      * In USB the host is the side that starts every transfer; a device
hw/usb/trace-events +6 -0
index 549f5f6e23..fe73ef6698 100644
@@ -424,3 +424,9 @@ usbredir_server_nak_park(uint64_t id, int ep) "id %" PRIu64 " ep %d"
 usbredir_server_nak_retry(uint64_t id, int ep) "id %" PRIu64 " ep %d"
 usbredir_server_ctrl_complete(uint64_t id, int status, int actual) "id %" PRIu64 " status %d actual %d"
 usbredir_server_ctrl_refused(uint64_t id, int status) "id %" PRIu64 " status %d"
+usbredir_server_bulk(uint64_t id, uint8_t ep, size_t len) "id %" PRIu64 " ep 0x%02x len %zu"
+usbredir_server_bulk_complete(uint64_t id, uint8_t ep, int status, int actual) "id %" PRIu64 " ep 0x%02x status %d actual %d"
+usbredir_server_bulk_too_big(uint64_t id, uint8_t ep, uint32_t len) "id %" PRIu64 " ep 0x%02x len %u"
+usbredir_server_refused(uint64_t id, uint8_t ep, int status, uint8_t want, uint8_t got) "id %" PRIu64 " ep 0x%02x status %d, wanted type %u, it is %u"
+usbredir_server_interrupt(uint64_t id, uint8_t ep, size_t len) "id %" PRIu64 " ep 0x%02x len %zu"
+usbredir_server_interrupt_complete(uint64_t id, uint8_t ep, int status, int actual) "id %" PRIu64 " ep 0x%02x status %d actual %d"
Signature · 1 line
2.43.0
author
#17 lore raw

PATCH v9 17/19 hw/usb/redirect-server: Stream interrupt IN endpoints

In reply to Jamin Lin

Interrupt IN is not a request and an answer in usbredir. The host asks once with start_interrupt_receiving. After that it expects a packet every time the device has data.

Park one IN packet on the device for each streaming endpoint. A device that answers USB_RET_ASYNC holds the packet and completes it when it has data. A device that answers USB_RET_NAK gives the packet back at once, so park a new one when the bus reports a wakeup on that endpoint. A slow timer parks one too, for a device that does not report a wakeup. A streamed request does not wait on the endpoint's list the way a host request does, because this timer already asks again.

Wait for the endpoint's bInterval before parking the next one. Some devices answer every time. Without the wait we would ask as fast as the main loop runs, and the guest would get no time.

On an error other than a stall the stream stays up, but back off to the slow timer. At a high speed bInterval of 125us an endpoint that keeps failing would otherwise send thousands of status messages a second.

Give every endpoint its own timer and bottom half. We ask each endpoint at its own rate. Drop the timer when the bottom half has already asked, so it does not fire for nothing.

---
 hw/usb/redirect-server.c | 309 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |   4 +
 2 files changed, 308 insertions(+), 5 deletions(-)
hw/usb/redirect-server.c +304 -5
index dce92eb263..2abbec74c6 100644
@@ -123,6 +123,9 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
  */
 #define USBREDIR_SERVER_MAX_BULK (1 * MiB)
 
+/* Buffer size for an interrupt IN endpoint before its descriptor is seen. */
+#define USBREDIR_SERVER_INTR_STREAM_DEFAULT_LEN 64
+
 /*
  * The steps to read the descriptors, in order:
  *
@@ -153,6 +156,7 @@ typedef enum {
     USBREDIR_SERVER_DESCRIPTOR,
     USBREDIR_SERVER_BULK,
     USBREDIR_SERVER_INTERRUPT,
+    USBREDIR_SERVER_INTR_STREAM,
 } USBRedirServerPktType;
 
 /* Which message answers the host when a control transfer ends. */
@@ -211,6 +215,15 @@ typedef struct USBRedirServerEp {
     /* Asks again for a device that does not wake the endpoint. */
     QEMUTimer *retry;
 
+    /*
+     * Interrupt IN streaming. The timer asks the device again at the
+     * endpoint's own rate, and more slowly after a NAK or an error.
+     * The bottom half asks at once when the device says it has data.
+     */
+    QEMUTimer *intr_stream_timer;
+    QEMUBH *intr_stream_bh;
+    bool intr_streaming;
+
     uint16_t max_packet;
     uint8_t interval;
     uint8_t iface;
@@ -255,7 +268,7 @@ struct USBRedirServer {
     bool desc_answered;
     QEMUBH *desc_bh;
 
-    /* Every request the host is still waiting for an answer to. */
+    /* Every request waiting for the device to answer. */
     QTAILQ_HEAD(, USBRedirServerPkt) inflight;
     uint64_t next_id;
 
@@ -1176,6 +1189,97 @@ static void usbredir_server_interrupt_complete(USBRedirServer *s,
                                     rp->data, actual);
 }
 
+/*
+ * How long to wait before asking the device again. bInterval means
+ * different things at different speeds: a number of frames at low and
+ * full speed, an exponent at high speed.
+ */
+static int64_t usbredir_server_intr_stream_period_ns(USBRedirServer *s,
+                                                     int ep_nr)
+{
+    int idx = usbredir_server_ep_index(ep_nr | USB_DIR_IN);
+    USBDevice *device = usbredir_server_device(s);
+    uint8_t interval = s->ep[idx].interval;
+
+    if (interval == 0) {
+        interval = 1;
+    }
+
+    if (device && device->speed == USB_SPEED_HIGH) {
+        interval = MIN(interval, 16);
+        return (1LL << (interval - 1)) * 125 * SCALE_US;
+    }
+
+    return (int64_t)interval * SCALE_MS;
+}
+
+/*
+ * The device answered a streamed request. Send the answer to the host, then
+ * ask again, because the host still wants more. Do not ask from here: the
+ * device may answer at once, and this function would call itself over and
+ * over. Arm the timer instead. It also waits for the endpoint's bInterval.
+ */
+static void usbredir_server_intr_stream_complete(USBRedirServer *s,
+                                                 USBRedirServerPkt *rp)
+{
+    struct usb_redir_interrupt_packet_header resp = rp->intr_hdr;
+    struct usb_redir_interrupt_receiving_status_header st;
+    int idx = usbredir_server_ep_index(resp.endpoint);
+    int ep_nr = resp.endpoint & 0x0f;
+    USBRedirServerEp *ep = &s->ep[idx];
+    USBPacket *p = &rp->pkt;
+    int actual = p->actual_length;
+    bool retry = false;
+    int64_t delay_ns;
+
+    trace_usbredir_server_intr_stream_complete(ep_nr, p->status, actual);
+
+    switch (p->status) {
+    case USB_RET_SUCCESS:
+        resp.status = usb_redir_success;
+        resp.length = actual;
+        usbredirparser_send_interrupt_packet(s->parser, 0, &resp,
+                                             actual ? rp->data : NULL,
+                                             actual);
+        usbredirparser_do_write(s->parser);
+        break;
+    case USB_RET_NAK:
+        /* no data yet; wait, unless the device wakes us first */
+        retry = true;
+        break;
+    default:
+        /*
+         * The endpoint failed. There is no data, so send a status
+         * message instead. The host gives it to its guest.
+         *
+         * redirect.c ends the stream only on a stall, so do the same
+         * here. If we end it on any other error, the host still
+         * thinks the stream runs, and it never asks again.
+         */
+        st.endpoint = resp.endpoint;
+        st.status = usbredir_server_status(p->status);
+        usbredirparser_send_interrupt_receiving_status(s->parser, 0, &st);
+        usbredirparser_do_write(s->parser);
+        if (p->status == USB_RET_STALL) {
+            ep->intr_streaming = false;
+        } else {
+            /* the stream stays up, but do not ask at the endpoint's rate */
+            retry = true;
+        }
+        break;
+    }
+
+    if (ep->intr_streaming) {
+        if (retry) {
+            delay_ns = (int64_t)USBREDIR_SERVER_RETRY_MS * SCALE_MS;
+        } else {
+            delay_ns = usbredir_server_intr_stream_period_ns(s, ep_nr);
+        }
+        timer_mod(ep->intr_stream_timer,
+                  qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + delay_ns);
+    }
+}
+
 /*
  * USB port ops
  */
@@ -1273,6 +1377,9 @@ static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
     case USBREDIR_SERVER_INTERRUPT:
         usbredir_server_interrupt_complete(s, rp);
         break;
+    case USBREDIR_SERVER_INTR_STREAM:
+        usbredir_server_intr_stream_complete(s, rp);
+        break;
     default:
         g_assert_not_reached();
     }
@@ -1312,9 +1419,11 @@ static USBRedirServerAsk usbredir_server_ask_device(USBRedirServer *s,
 
     /*
      * Keep the request and send it again when the device wakes the
-     * endpoint.
+     * endpoint. A streamed request asks again by itself, with its own
+     * timer.
      */
-    if (rp->pkt.status == USB_RET_NAK) {
+    if (rp->pkt.status == USB_RET_NAK &&
+        rp->type != USBREDIR_SERVER_INTR_STREAM) {
         trace_usbredir_server_nak_park(rp->redir_id, rp->ep_idx);
         QTAILQ_INSERT_HEAD(&ep->waiting, rp, waiting_link);
         timer_mod(ep->retry, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
@@ -1360,6 +1469,15 @@ static void usbredir_server_wakeup_ep(USBBus *bus, USBEndpoint *ep,
 {
     USBRedirServer *s = container_of(bus, USBRedirServer, bus);
     uint8_t addr = ep->nr | (ep->pid == USB_TOKEN_IN ? USB_DIR_IN : 0);
+    int idx = usbredir_server_ep_index(addr);
+
+    /*
+     * The device has data. A streaming interrupt IN endpoint may have
+     * answered NAK. Ask now instead of waiting for the retry timer.
+     */
+    if (s->ep[idx].intr_streaming) {
+        qemu_bh_schedule(s->ep[idx].intr_stream_bh);
+    }
 
     /*
      * Our descriptor request is not on inflight or waiting, so
@@ -1371,7 +1489,8 @@ static void usbredir_server_wakeup_ep(USBBus *bus, USBEndpoint *ep,
         qemu_bh_schedule(s->desc_bh);
     }
 
-    usbredir_server_kick_ep(s, usbredir_server_ep_index(addr));
+    /* The host may also have a request stopped on this endpoint. */
+    usbredir_server_kick_ep(s, idx);
 }
 
 static USBBusOps usbredir_server_bus_ops = {
@@ -1391,6 +1510,95 @@ static void usbredir_server_submit(USBRedirServer *s, USBRedirServerPkt *rp)
     usbredir_server_ask_device(s, rp);
 }
 
+/*
+ * Interrupt IN streaming
+ */
+
+static bool usbredir_server_intr_stream_inflight(USBRedirServer *s, int ep_nr)
+{
+    USBRedirServerPkt *rp;
+
+    QTAILQ_FOREACH(rp, &s->inflight, inflight_link) {
+        if (rp->type == USBREDIR_SERVER_INTR_STREAM &&
+            (rp->intr_hdr.endpoint & 0x0f) == ep_nr) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/*
+ * Ask the device for data on @ep_nr and leave the request waiting. A device
+ * sends data only when asked. Do nothing if a request on @ep_nr is still
+ * unanswered.
+ */
+static void usbredir_server_intr_stream_park(USBRedirServer *s, int ep_nr)
+{
+    USBDevice *device = usbredir_server_device(s);
+    int idx = usbredir_server_ep_index(ep_nr | USB_DIR_IN);
+    USBRedirServerPkt *rp;
+    USBEndpoint *ep;
+    int len;
+
+    /* No device to ask. */
+    if (!device || !device->attached) {
+        return;
+    }
+
+    /* The host did not ask for this endpoint, or a request is unanswered. */
+    if (!s->ep[idx].intr_streaming ||
+        usbredir_server_intr_stream_inflight(s, ep_nr)) {
+        return;
+    }
+
+    len = s->ep[idx].max_packet;
+    if (len == 0) {
+        len = USBREDIR_SERVER_INTR_STREAM_DEFAULT_LEN;
+    }
+    ep = usb_ep_get(device, USB_TOKEN_IN, ep_nr);
+    rp = usbredir_server_pkt_alloc(len, idx);
+    rp->type = USBREDIR_SERVER_INTR_STREAM;
+    /* A streamed request comes from us, not from the host. */
+    rp->redir_id = 0;
+    rp->intr_hdr.endpoint = ep_nr | USB_DIR_IN;
+
+    usb_packet_setup(&rp->pkt, USB_TOKEN_IN, ep, 0, s->next_id++,
+                     false, false);
+    usb_packet_addbuf(&rp->pkt, rp->data, len);
+
+    /*
+     * The timer is there for a device that does not wake the endpoint.
+     * We are asking now, so drop it. The answer sets it again.
+     */
+    timer_del(s->ep[idx].intr_stream_timer);
+
+    trace_usbredir_server_intr_stream_park(ep_nr, len);
+    usbredir_server_submit(s, rp);
+}
+
+/* BH and timer callback: ask again on one streaming endpoint. */
+static void usbredir_server_intr_stream_kick(void *opaque)
+{
+    USBRedirServerEp *ep = opaque;
+    int ep_nr = ep->idx - USBREDIR_SERVER_EP_IN_BASE;
+
+    usbredir_server_intr_stream_park(ep->s, ep_nr);
+}
+
+static void usbredir_server_intr_stream_cancel(USBRedirServer *s, int ep_nr)
+{
+    USBRedirServerPkt *tmp;
+    USBRedirServerPkt *rp;
+
+    QTAILQ_FOREACH_SAFE(rp, &s->inflight, inflight_link, tmp) {
+        if (rp->type != USBREDIR_SERVER_INTR_STREAM ||
+            (rp->intr_hdr.endpoint & 0x0f) != ep_nr) {
+            continue;
+        }
+        usbredir_server_pkt_free(s, rp);
+    }
+}
+
 /*
  * usbredirparser I/O and logging callbacks
  */
@@ -1816,6 +2024,63 @@ static void usbredir_server_interface_info(void *priv,
     /* The host should not send this to a device. Nothing to do. */
 }
 
+/*
+ * The host asks to stream an interrupt IN endpoint. Send the receiving
+ * status first. Without that status the host throws the interrupt
+ * packet away. Then ask the device once.
+ */
+static void usbredir_server_start_interrupt_receiving(void *priv,
+    uint64_t id, struct usb_redir_start_interrupt_receiving_header *hdr)
+{
+    struct usb_redir_interrupt_receiving_status_header st = {
+        .endpoint = hdr->endpoint,
+        .status = usb_redir_success,
+    };
+    int idx = usbredir_server_ep_index(hdr->endpoint);
+    int ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServer *s = priv;
+
+    st.status = usbredir_server_ep_status(s, hdr->endpoint,
+                                          USB_ENDPOINT_XFER_INT);
+
+    /* Only an interrupt IN endpoint can stream. */
+    if (!(hdr->endpoint & USB_DIR_IN)) {
+        st.status = usb_redir_inval;
+    }
+
+    if (st.status == usb_redir_success) {
+        s->ep[idx].intr_streaming = true;
+    }
+
+    trace_usbredir_server_intr_stream_start(hdr->endpoint, st.status);
+    usbredirparser_send_interrupt_receiving_status(s->parser, id, &st);
+    usbredirparser_do_write(s->parser);
+
+    if (st.status == usb_redir_success) {
+        usbredir_server_intr_stream_park(s, ep_nr);
+    }
+}
+
+static void usbredir_server_stop_interrupt_receiving(void *priv, uint64_t id,
+    struct usb_redir_stop_interrupt_receiving_header *hdr)
+{
+    int idx = usbredir_server_ep_index(hdr->endpoint);
+    int ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServer *s = priv;
+
+    trace_usbredir_server_intr_stream_stop(hdr->endpoint);
+
+    /* Endpoint 0 is control, and an OUT endpoint never streams. */
+    if (ep_nr == 0 || !(hdr->endpoint & USB_DIR_IN)) {
+        return;
+    }
+
+    s->ep[idx].intr_streaming = false;
+    qemu_bh_cancel(s->ep[idx].intr_stream_bh);
+    timer_del(s->ep[idx].intr_stream_timer);
+    usbredir_server_intr_stream_cancel(s, ep_nr);
+}
+
 static void usbredir_server_alloc_bulk_streams(void *priv, uint64_t id,
     struct usb_redir_alloc_bulk_streams_header *hdr)
 {
@@ -1841,6 +2106,14 @@ static void usbredir_server_cancel_data_packet(void *priv, uint64_t id)
     int idx;
 
     QTAILQ_FOREACH(rp, &s->inflight, inflight_link) {
+        /*
+         * A streamed request comes from us, not from the host.
+         * The host has no id for it, so a cancel never matches it.
+         */
+        if (rp->type == USBREDIR_SERVER_INTR_STREAM) {
+            continue;
+        }
+
         if (rp->redir_id == id) {
             idx = rp->ep_idx;
 
@@ -1918,6 +2191,12 @@ static void usbredir_server_stop_transfers(USBRedirServer *s)
         timer_del(s->ep[i].retry);
     }
 
+    for (i = USBREDIR_SERVER_EP_IN_BASE + 1; i < USBREDIR_SERVER_MAX_EP; i++) {
+        s->ep[i].intr_streaming = false;
+        qemu_bh_cancel(s->ep[i].intr_stream_bh);
+        timer_del(s->ep[i].intr_stream_timer);
+    }
+
     /*
      * The host waits for an answer to each of these. Send cancelled
      * before we free them.
@@ -1966,6 +2245,10 @@ static void usbredir_server_create_parser(USBRedirServer *s)
     s->parser->device_disconnect_ack_func =
         usbredir_server_device_disconnect_ack;
     s->parser->interface_info_func = usbredir_server_interface_info;
+    s->parser->start_interrupt_receiving_func =
+        usbredir_server_start_interrupt_receiving;
+    s->parser->stop_interrupt_receiving_func =
+        usbredir_server_stop_interrupt_receiving;
     s->parser->alloc_bulk_streams_func = usbredir_server_alloc_bulk_streams;
     s->parser->cancel_data_packet_func = usbredir_server_cancel_data_packet;
     s->parser->start_bulk_receiving_func =
@@ -2115,6 +2398,16 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
         s->ep[i].retry = timer_new_ns(QEMU_CLOCK_VIRTUAL,
                                       usbredir_server_retry_ep, &s->ep[i]);
     }
+
+    /* Only an interrupt IN endpoint streams, so endpoint 0 and OUT are out. */
+    for (i = USBREDIR_SERVER_EP_IN_BASE + 1; i < USBREDIR_SERVER_MAX_EP; i++) {
+        s->ep[i].intr_stream_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+                                             usbredir_server_intr_stream_kick,
+                                             &s->ep[i]);
+        s->ep[i].intr_stream_bh =
+            qemu_bh_new_guarded(usbredir_server_intr_stream_kick, &s->ep[i],
+                                &dev->mem_reentrancy_guard);
+    }
     s->config_table = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                                             NULL, g_free);
     usbredir_server_clear_ep_table(s);
@@ -2125,7 +2418,6 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
     usb_register_port(&s->bus, &s->port, s, 0, &usbredir_server_port_ops,
                       USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL |
                       USB_SPEED_MASK_HIGH);
-
     s->chardev_close_bh = qemu_bh_new_guarded(usbredir_server_chardev_close_bh,
                                               s, &dev->mem_reentrancy_guard);
     s->announce_bh = qemu_bh_new_guarded(usbredir_server_announce_bh, s,
@@ -2178,6 +2470,13 @@ static void usbredir_server_unrealize(DeviceState *dev)
         timer_free(s->ep[i].retry);
         s->ep[i].retry = NULL;
     }
+
+    for (i = USBREDIR_SERVER_EP_IN_BASE + 1; i < USBREDIR_SERVER_MAX_EP; i++) {
+        timer_free(s->ep[i].intr_stream_timer);
+        s->ep[i].intr_stream_timer = NULL;
+        qemu_bh_delete(s->ep[i].intr_stream_bh);
+        s->ep[i].intr_stream_bh = NULL;
+    }
 }
 
 static const Property usbredir_server_props[] = {
hw/usb/trace-events +4 -0
index fe73ef6698..20c910e28a 100644
@@ -430,3 +430,7 @@ usbredir_server_bulk_too_big(uint64_t id, uint8_t ep, uint32_t len) "id %" PRIu6
 usbredir_server_refused(uint64_t id, uint8_t ep, int status, uint8_t want, uint8_t got) "id %" PRIu64 " ep 0x%02x status %d, wanted type %u, it is %u"
 usbredir_server_interrupt(uint64_t id, uint8_t ep, size_t len) "id %" PRIu64 " ep 0x%02x len %zu"
 usbredir_server_interrupt_complete(uint64_t id, uint8_t ep, int status, int actual) "id %" PRIu64 " ep 0x%02x status %d actual %d"
+usbredir_server_intr_stream_start(uint8_t ep, int status) "start_interrupt_receiving ep 0x%02x -> status %d"
+usbredir_server_intr_stream_stop(uint8_t ep) "stop_interrupt_receiving ep 0x%02x"
+usbredir_server_intr_stream_park(unsigned ep_nr, int len) "parked IN packet ep %u len %d"
+usbredir_server_intr_stream_complete(unsigned ep_nr, int status, int actual) "ep %u status %d actual %d"
Signature · 1 line
2.43.0
author
#18 lore raw

PATCH v9 18/19 hw/usb/redirect-server: Implement isochronous OUT transfers

In reply to Jamin Lin

The protocol has three callbacks for isochronous transfers:
start_iso_stream_func, stop_iso_stream_func and iso_packet_func. Isochronous has no capability bit, and the parser calls them without checking, so a server cannot leave them out.

Implement the three for an OUT endpoint. Endpoint 0 is control, so refuse it. The host sends a packet for every OUT transfer. It sends nothing for an IN transfer, so refuse an IN endpoint with a stall. Only a stall makes the host close the stream. Any other status leaves it open on the host side.

An isochronous packet has no id, and the host wants no answer. Report a failure with an iso_stream_status. A broken endpoint fails every frame, so send at most one report a second. A stall does not wait. A stall ends the stream, and the host has to hear it.

With no id, a cancel never matches an isochronous packet. A NAK does not park it for a retry either.

---
 hw/usb/redirect-server.c | 161 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |   5 ++
 2 files changed, 162 insertions(+), 4 deletions(-)
hw/usb/redirect-server.c +157 -4
index 2abbec74c6..65fc9788cd 100644
@@ -126,6 +126,9 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
 /* Buffer size for an interrupt IN endpoint before its descriptor is seen. */
 #define USBREDIR_SERVER_INTR_STREAM_DEFAULT_LEN 64
 
+/* The shortest time between two failure reports. */
+#define USBREDIR_SERVER_ISO_STREAM_STATUS_MS 1000
+
 /*
  * The steps to read the descriptors, in order:
  *
@@ -157,6 +160,7 @@ typedef enum {
     USBREDIR_SERVER_BULK,
     USBREDIR_SERVER_INTERRUPT,
     USBREDIR_SERVER_INTR_STREAM,
+    USBREDIR_SERVER_ISO_STREAM,
 } USBRedirServerPktType;
 
 /* Which message answers the host when a control transfer ends. */
@@ -183,6 +187,7 @@ typedef struct USBRedirServerPkt {
     struct usb_redir_interrupt_packet_header intr_hdr;
     struct usb_redir_control_packet_header ctrl_hdr;
     struct usb_redir_bulk_packet_header bulk_hdr;
+    struct usb_redir_iso_packet_header iso_hdr;
 
     /*
      * The IOV points here. IN data lands in it, OUT data is copied in.
@@ -224,6 +229,13 @@ typedef struct USBRedirServerEp {
     QEMUBH *intr_stream_bh;
     bool intr_streaming;
 
+    /*
+     * Isochronous OUT streaming. The next failure report goes out at
+     * this time.
+     */
+    int64_t iso_status_next_ns;
+    bool iso_streaming;
+
     uint16_t max_packet;
     uint8_t interval;
     uint8_t iface;
@@ -1280,6 +1292,47 @@ static void usbredir_server_intr_stream_complete(USBRedirServer *s,
     }
 }
 
+/*
+ * Isochronous transfers have no answer and no retry, so a good packet
+ * needs nothing back.
+ *
+ * A broken endpoint fails every frame, so we do not report every
+ * failure.
+ */
+static void usbredir_server_iso_stream_complete(USBRedirServer *s,
+                                                USBRedirServerPkt *rp)
+{
+    struct usb_redir_iso_stream_status_header st;
+    USBRedirServerEp *ep = &s->ep[rp->ep_idx];
+    USBPacket *p = &rp->pkt;
+    int64_t now_ns;
+
+    trace_usbredir_server_iso_stream_complete(rp->iso_hdr.endpoint, p->status,
+                                              p->actual_length);
+
+    switch (p->status) {
+    case USB_RET_SUCCESS:
+        return;
+    case USB_RET_STALL:
+        /* The stream ends here, so the host has to hear this one. */
+        ep->iso_streaming = false;
+        break;
+    default:
+        now_ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
+        if (now_ns < ep->iso_status_next_ns) {
+            return;
+        }
+        ep->iso_status_next_ns = now_ns +
+            (int64_t)USBREDIR_SERVER_ISO_STREAM_STATUS_MS * SCALE_MS;
+        break;
+    }
+
+    st.endpoint = rp->iso_hdr.endpoint;
+    st.status = usbredir_server_status(p->status);
+    usbredirparser_send_iso_stream_status(s->parser, 0, &st);
+    usbredirparser_do_write(s->parser);
+}
+
 /*
  * USB port ops
  */
@@ -1380,6 +1433,9 @@ static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
     case USBREDIR_SERVER_INTR_STREAM:
         usbredir_server_intr_stream_complete(s, rp);
         break;
+    case USBREDIR_SERVER_ISO_STREAM:
+        usbredir_server_iso_stream_complete(s, rp);
+        break;
     default:
         g_assert_not_reached();
     }
@@ -1419,11 +1475,12 @@ static USBRedirServerAsk usbredir_server_ask_device(USBRedirServer *s,
 
     /*
      * Keep the request and send it again when the device wakes the
-     * endpoint. A streamed request asks again by itself, with its own
-     * timer.
+     * endpoint. A streamed request does not wait here. The interrupt
+     * stream has a timer of its own, and isochronous never retries.
      */
     if (rp->pkt.status == USB_RET_NAK &&
-        rp->type != USBREDIR_SERVER_INTR_STREAM) {
+        rp->type != USBREDIR_SERVER_INTR_STREAM &&
+        rp->type != USBREDIR_SERVER_ISO_STREAM) {
         trace_usbredir_server_nak_park(rp->redir_id, rp->ep_idx);
         QTAILQ_INSERT_HEAD(&ep->waiting, rp, waiting_link);
         timer_mod(ep->retry, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
@@ -2081,6 +2138,93 @@ static void usbredir_server_stop_interrupt_receiving(void *priv, uint64_t id,
     usbredir_server_intr_stream_cancel(s, ep_nr);
 }
 
+static void usbredir_server_start_iso_stream(void *priv, uint64_t id,
+    struct usb_redir_start_iso_stream_header *hdr)
+{
+    int idx = usbredir_server_ep_index(hdr->endpoint);
+    struct usb_redir_iso_stream_status_header st = {
+        .endpoint = hdr->endpoint,
+    };
+    USBRedirServer *s = priv;
+
+    st.status = usbredir_server_ep_status(s, hdr->endpoint,
+                                          USB_ENDPOINT_XFER_ISOC);
+
+    /*
+     * Only an OUT stream works here. Only a stall makes the host
+     * close the stream.
+     */
+    if (hdr->endpoint & USB_DIR_IN) {
+        st.status = usb_redir_stall;
+    }
+
+    if (st.status == usb_redir_success) {
+        s->ep[idx].iso_streaming = true;
+        s->ep[idx].iso_status_next_ns = 0;
+    }
+
+    trace_usbredir_server_iso_stream_start(hdr->endpoint, hdr->pkts_per_urb,
+                                           hdr->no_urbs, st.status);
+    usbredirparser_send_iso_stream_status(s->parser, id, &st);
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_stop_iso_stream(void *priv, uint64_t id,
+    struct usb_redir_stop_iso_stream_header *hdr)
+{
+    int idx = usbredir_server_ep_index(hdr->endpoint);
+    int ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServer *s = priv;
+
+    trace_usbredir_server_iso_stream_stop(hdr->endpoint);
+
+    /* Endpoint 0 is control, and an IN endpoint never streams. */
+    if (ep_nr == 0 || (hdr->endpoint & USB_DIR_IN)) {
+        return;
+    }
+
+    s->ep[idx].iso_streaming = false;
+}
+
+/* An isochronous packet has no id, and the host wants no answer. */
+static void usbredir_server_iso_packet(void *priv, uint64_t id,
+    struct usb_redir_iso_packet_header *hdr, uint8_t *data, int data_len)
+{
+    int idx = usbredir_server_ep_index(hdr->endpoint);
+    USBDevice *device = usbredir_server_device(priv);
+    int ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServer *s = priv;
+    USBRedirServerPkt *rp;
+    USBEndpoint *ep;
+    uint8_t status;
+
+    status = usbredir_server_ep_status(s, hdr->endpoint,
+                                       USB_ENDPOINT_XFER_ISOC);
+
+    if (status != usb_redir_success || !s->ep[idx].iso_streaming) {
+        trace_usbredir_server_iso_stream_dropped(hdr->endpoint, data_len);
+        usbredirparser_free_packet_data(s->parser, data);
+        return;
+    }
+
+    ep = usb_ep_get(device, USB_TOKEN_OUT, ep_nr);
+    rp = usbredir_server_pkt_alloc(data_len, idx);
+    rp->type = USBREDIR_SERVER_ISO_STREAM;
+    rp->iso_hdr = *hdr;
+    rp->redir_id = 0;
+
+    usb_packet_setup(&rp->pkt, USB_TOKEN_OUT, ep, 0, s->next_id++,
+                     false, false);
+    if (data_len > 0) {
+        memcpy(rp->data, data, data_len);
+    }
+    usb_packet_addbuf(&rp->pkt, rp->data, data_len);
+
+    trace_usbredir_server_iso_stream_packet(hdr->endpoint, data_len);
+    usbredirparser_free_packet_data(s->parser, data);
+    usbredir_server_submit(s, rp);
+}
+
 static void usbredir_server_alloc_bulk_streams(void *priv, uint64_t id,
     struct usb_redir_alloc_bulk_streams_header *hdr)
 {
@@ -2110,7 +2254,8 @@ static void usbredir_server_cancel_data_packet(void *priv, uint64_t id)
          * A streamed request comes from us, not from the host.
          * The host has no id for it, so a cancel never matches it.
          */
-        if (rp->type == USBREDIR_SERVER_INTR_STREAM) {
+        if (rp->type == USBREDIR_SERVER_INTR_STREAM ||
+            rp->type == USBREDIR_SERVER_ISO_STREAM) {
             continue;
         }
 
@@ -2197,6 +2342,11 @@ static void usbredir_server_stop_transfers(USBRedirServer *s)
         timer_del(s->ep[i].intr_stream_timer);
     }
 
+    for (i = 1; i < USBREDIR_SERVER_EP_IN_BASE; i++) {
+        s->ep[i].iso_streaming = false;
+        s->ep[i].iso_status_next_ns = 0;
+    }
+
     /*
      * The host waits for an answer to each of these. Send cancelled
      * before we free them.
@@ -2249,6 +2399,9 @@ static void usbredir_server_create_parser(USBRedirServer *s)
         usbredir_server_start_interrupt_receiving;
     s->parser->stop_interrupt_receiving_func =
         usbredir_server_stop_interrupt_receiving;
+    s->parser->start_iso_stream_func = usbredir_server_start_iso_stream;
+    s->parser->stop_iso_stream_func = usbredir_server_stop_iso_stream;
+    s->parser->iso_packet_func = usbredir_server_iso_packet;
     s->parser->alloc_bulk_streams_func = usbredir_server_alloc_bulk_streams;
     s->parser->cancel_data_packet_func = usbredir_server_cancel_data_packet;
     s->parser->start_bulk_receiving_func =
hw/usb/trace-events +5 -0
index 20c910e28a..36a870689f 100644
@@ -434,3 +434,8 @@ usbredir_server_intr_stream_start(uint8_t ep, int status) "start_interrupt_recei
 usbredir_server_intr_stream_stop(uint8_t ep) "stop_interrupt_receiving ep 0x%02x"
 usbredir_server_intr_stream_park(unsigned ep_nr, int len) "parked IN packet ep %u len %d"
 usbredir_server_intr_stream_complete(unsigned ep_nr, int status, int actual) "ep %u status %d actual %d"
+usbredir_server_iso_stream_start(uint8_t ep, uint8_t pkts, uint8_t urbs, int status) "start_iso_stream ep 0x%02x pkts_per_urb %u no_urbs %u -> status %d"
+usbredir_server_iso_stream_stop(uint8_t ep) "stop_iso_stream ep 0x%02x"
+usbredir_server_iso_stream_packet(uint8_t ep, int len) "iso_packet ep 0x%02x len %d"
+usbredir_server_iso_stream_dropped(uint8_t ep, int len) "iso_packet dropped ep 0x%02x len %d"
+usbredir_server_iso_stream_complete(uint8_t ep, int status, int actual) "iso ep 0x%02x status %d actual %d"
Signature · 1 line
2.43.0
author
#19 lore raw

PATCH v9 19/19 hw/arm/aspeed: Enable the usbredir server transport

In reply to Jamin Lin

Let the ASPEED machines accept "x-usb-redir-server" with -device, so a UDC gadget can be exported to a USB host running in another QEMU:

-device x-usb-redir-server,id=redirsrv,chardev=<chr> -device aspeed.udc-gadget,bus=redirsrv.0,udc=/machine/soc/udc

x-usb-redir-server sits on the main system bus so that the USB bus it provides can be found by "bus=<id>.0". That makes it a sysbus device, and a sysbus device can only be created with -device if the machine lists it.

---
 hw/arm/aspeed.c | 7 +++++++
 1 file changed, 7 insertions(+)
hw/arm/aspeed.c +7 -0
index a7cfbd6c5e..fc27ac1788 100644
@@ -409,6 +409,13 @@ static void aspeed_machine_class_init(ObjectClass *oc, const void *data)
     mc->no_cdrom = 1;
     mc->no_parallel = 1;
     mc->default_ram_id = "ram";
+    /*
+     * Lets a UDC gadget be exported to another QEMU instance over usbredir.
+     * The type name is spelled out rather than used through its macro so
+     * that this still builds when the usbredir library is absent.
+     */
+    machine_class_allow_dynamic_sysbus_dev(mc,
+                                           "x-usb-redir-server");
     amc->macs_mask = ASPEED_MAC0_ON;
     amc->uart_default = ASPEED_DEV_UART5;
 
Signature · 1 line
2.43.0