All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v7 0/8] hw/usb: Add a usbredir server transport
@ 2026-10-28  3:41 Jamin Lin
  2026-10-28  3:41 ` [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub Jamin Lin
                   ` (7 more replies)
  0 siblings, 8 replies; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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/20260902021542.3194812-1-jamin_lin@aspeedtech.com/

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.
    
Jamin Lin (8):
  hw/usb/bus: Let the controller ask for the automatic hub
  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: Implement bulk and interrupt transfers
  hw/usb/redirect-server: Stream interrupt IN endpoints
  hw/arm/aspeed: Enable the usbredir server transport

 include/hw/usb/usb.h     |    1 +
 hw/arm/aspeed.c          |    7 +
 hw/usb/bus.c             |    6 +-
 hw/usb/hcd-dwc2.c        |    1 +
 hw/usb/hcd-ohci.c        |    1 +
 hw/usb/hcd-uhci.c        |    1 +
 hw/usb/redirect-server.c | 1893 ++++++++++++++++++++++++++++++++++++++
 hw/usb/meson.build       |    3 +-
 hw/usb/trace-events      |   34 +
 9 files changed, 1945 insertions(+), 2 deletions(-)
 create mode 100644 hw/usb/redirect-server.c

-- 
2.53.0


^ permalink raw reply	[flat|nested] 18+ messages in thread

* [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  2026-10-29 10:09   ` Junjie Cao
  2026-10-29 17:44   ` marcandre.lureau
  2026-10-28  3:41 ` [PATCH v7 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
                   ` (6 subsequent siblings)
  7 siblings, 2 replies; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

usb_claim_port() plugs in a usb-hub when only one port is free, so that
more devices fit. Every bus gets that today. A controller with a single
root port then puts its only device behind a hub, and a transport that
carries one device cannot use that.

The hub speaks USB 1.1, so this belongs to UHCI and OHCI. Add auto_hub
and leave it off by default. UHCI and OHCI set it, both when they own
the bus and when they join one as a companion. dwc2 sets it too, so the
boards that use it keep working.

xHCI has up to 15 root ports, so it no longer grows a hub on its own. A
user who wants more ports there adds "-device usb-hub".

Suggested-by: Gerd Hoffmann <kraxel@redhat.com>
Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 include/hw/usb/usb.h | 1 +
 hw/usb/bus.c         | 6 +++++-
 hw/usb/hcd-dwc2.c    | 1 +
 hw/usb/hcd-ohci.c    | 1 +
 hw/usb/hcd-uhci.c    | 1 +
 5 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/include/hw/usb/usb.h b/include/hw/usb/usb.h
index cbd4711933..cc1d6d74b1 100644
--- a/include/hw/usb/usb.h
+++ b/include/hw/usb/usb.h
@@ -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;
diff --git a/hw/usb/bus.c b/hw/usb/bus.c
index 5cc5ffec33..d1ad22ac94 100644
--- a/hw/usb/bus.c
+++ b/hw/usb/bus.c
@@ -363,6 +363,9 @@ void usb_register_companion(const char *masterbus, USBPort *ports[],
         return;
     }
 
+    /* Only UHCI and OHCI come in as a companion, and both want the hub. */
+    bus->auto_hub = true;
+
     for (i = 0; i < portcount; i++) {
         usb_fill_port(ports[i], opaque, i, ops, speedmask);
     }
@@ -413,7 +416,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) {
diff --git a/hw/usb/hcd-dwc2.c b/hw/usb/hcd-dwc2.c
index cd3b61158d..104f493105 100644
--- a/hw/usb/hcd-dwc2.c
+++ b/hw/usb/hcd-dwc2.c
@@ -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 = true;
     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));
diff --git a/hw/usb/hcd-ohci.c b/hw/usb/hcd-ohci.c
index 5e5fe41f47..d6b1fc5b62 100644
--- a/hw/usb/hcd-ohci.c
+++ b/hw/usb/hcd-ohci.c
@@ -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 = true;
         for (i = 0; i < num_ports; i++) {
             usb_register_port(&ohci->bus, &ohci->rhport[i].port,
                               ohci, i, &ohci_port_ops,
diff --git a/hw/usb/hcd-uhci.c b/hw/usb/hcd-uhci.c
index a7b9fe1317..f11664bd55 100644
--- a/hw/usb/hcd-uhci.c
+++ b/hw/usb/hcd-uhci.c
@@ -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 = true;
         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);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v7 2/8] hw/usb/redirect-server: Add a usbredir server device
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
  2026-10-28  3:41 ` [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  2026-10-28  3:41 ` [PATCH v7 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 hw/usb/redirect-server.c | 142 +++++++++++++++++++++++++++++++++++++++
 hw/usb/meson.build       |   3 +-
 2 files changed, 144 insertions(+), 1 deletion(-)
 create mode 100644 hw/usb/redirect-server.c

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
new file mode 100644
index 0000000000..8399bc2349
--- /dev/null
+++ b/hw/usb/redirect-server.c
@@ -0,0 +1,142 @@
+/*
+ * 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);
+    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)
diff --git a/hw/usb/meson.build b/hw/usb/meson.build
index 429dcd6a24..0061b935e6 100644
--- a/hw/usb/meson.build
+++ b/hw/usb/meson.build
@@ -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
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v7 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
  2026-10-28  3:41 ` [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub Jamin Lin
  2026-10-28  3:41 ` [PATCH v7 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  2026-10-28  3:41 ` [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 hw/usb/redirect-server.c | 285 +++++++++++++++++++++++++++++++++++++++
 hw/usb/trace-events      |   6 +
 2 files changed, 291 insertions(+)

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 8399bc2349..4971d81af4 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -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,233 @@ 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:
+     *   do_write() -> this -> qemu_chr_fe_write() -> chardev feeds us
+     *   -> do_read() -> a callback -> do_write() again
+     *
+     * The second 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);
+    /* 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,13 +353,45 @@ 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);
     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.
@@ -124,8 +407,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[] = {
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index 80ead23358..d141661673 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -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"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (2 preceding siblings ...)
  2026-10-28  3:41 ` [PATCH v7 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  2026-10-29 10:09   ` Junjie Cao
  2026-10-29 17:44   ` marcandre.lureau
  2026-10-28  3:41 ` [PATCH v7 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
                   ` (3 subsequent siblings)
  7 siblings, 2 replies; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 hw/usb/redirect-server.c | 130 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |   7 +++
 2 files changed, 136 insertions(+), 1 deletion(-)

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 4971d81af4..ad6e4c422b 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -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"
@@ -90,12 +91,88 @@ struct USBRedirServer {
     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_device(USBRedirServer *s)
+{
+    USBDevice *device = usbredir_server_device(s);
+    struct usb_redir_interface_info_header iface_info = {
+        .interface_count = 0,
+    };
+    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);
+
+    /* Put the device in DEFAULT state. The host may not reset the bus. */
+    device->addr = 0;
+    device->state = USB_STATE_DEFAULT;
+
+    /* Send this before device_connect. The peer needs it to accept us. */
+    usbredirparser_send_interface_info(s->parser, &iface_info);
+    usbredirparser_do_write(s->parser);
+
+    trace_usbredir_server_announce(conn.speed);
+    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();
+    usbredir_server_announce_device(s);
+}
+
+static void usbredir_server_port_detach(USBPort *port)
+{
+    USBRedirServer *s = port->opaque;
+
+    trace_usbredir_server_detach(s->device_announced);
+
+    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 +184,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,
 };
@@ -210,13 +289,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;
+    usbredir_server_announce_device(s);
+}
+
+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. */
 }
 
 /*
@@ -240,6 +359,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);
@@ -267,6 +394,7 @@ static void usbredir_server_create_parser(USBRedirServer *s)
 static void usbredir_server_destroy_parser(USBRedirServer *s)
 {
     s->host_connected = false;
+    s->device_announced = false;
 
     g_clear_handle_id(&s->watch, g_source_remove);
 
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index d141661673..2cc6a244b9 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -399,3 +399,10 @@ 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_announce(uint8_t speed) "device_connect speed %u"
+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"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v7 5/8] hw/usb/redirect-server: Implement control transfers
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (3 preceding siblings ...)
  2026-10-28  3:41 ` [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  2026-10-29 17:44   ` marcandre.lureau
  2026-10-28  3:41 ` [PATCH v7 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
                   ` (2 subsequent siblings)
  7 siblings, 1 reply; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 hw/usb/redirect-server.c | 813 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |  11 +
 2 files changed, 823 insertions(+), 1 deletion(-)

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index ad6e4c422b..61ac5473e5 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -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,83 @@ 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
+
+/*
+ * 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
+
+#define USBREDIR_SERVER_CTRL 0
+
+/* 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;
+
+    USBRedirServerReply reply;
+    uint64_t redir_id;
+    int ep_idx;
+    int type;
+} 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;
 
@@ -92,8 +172,22 @@ 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_descs;
+    uint8_t config_value;
 };
 
+static void usbredir_server_pkt_free(USBRedirServer *s, USBRedirServerPkt *rp);
+static void usbredir_server_stop_transfers(USBRedirServer *s);
+static void usbredir_server_send_cancelled(USBRedirServer *s,
+                                           USBRedirServerPkt *rp);
+
 /*
  * The device is whatever USBDevice the user plugged into our port with
  * "-device <device>,bus=<id>.0". NULL until then.
@@ -104,9 +198,212 @@ static USBDevice *usbredir_server_device(USBRedirServer *s)
 }
 
 /*
- * Device announcement
+ * Descriptor snooping and device announcement
  */
 
+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 || !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 void usbredir_server_save_config_desc(USBRedirServer *s,
+                                             const uint8_t *data, int len)
+{
+    const USBDescriptor *conf = (const USBDescriptor *)data;
+    uint16_t total;
+
+    if (len < 4) {
+        return;
+    }
+    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 < 9 || len < total) {
+        return;
+    }
+
+    g_hash_table_insert(s->config_descs,
+                        GUINT_TO_POINTER(conf->u.config.bConfigurationValue),
+                        g_memdup2(data, total));
+}
+
+/*
+ * Some devices only forward transfers. They never read their own
+ * descriptors, 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_build_ep_table(USBRedirServer *s)
+{
+    const USBDescriptor *desc;
+    const uint8_t *config;
+    bool active = false;
+    uint8_t iface = 0;
+    uint8_t alt;
+    int i = 0;
+    int total;
+
+    usbredir_server_clear_ep_table(s);
+
+    config = g_hash_table_lookup(s->config_descs,
+                                 GUINT_TO_POINTER(s->config_value));
+    if (!config) {
+        trace_usbredir_server_config_not_found(
+            s->config_value, g_hash_table_size(s->config_descs));
+        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 < 9) {
+                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];
+            break;
+        case USB_DT_ENDPOINT:
+            if (desc->bLength < 7) {
+                trace_usbredir_server_bad_descriptor(i, desc->bLength,
+                                                     desc->bDescriptorType);
+                break;
+            }
+            if (active) {
+                usbredir_server_record_endpoint(s, desc, iface);
+            }
+            break;
+        }
+
+        i += desc->bLength;
+    }
+}
+
+static void usbredir_server_send_ep_info(USBRedirServer *s)
+{
+    struct usb_redir_ep_info_header ep_info = {};
+    int i;
+
+    usbredir_server_build_ep_table(s);
+
+    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 uint8_t usbredir_server_speed(USBDevice *device)
 {
     switch (device->speed) {
@@ -145,6 +442,108 @@ static void usbredir_server_announce_device(USBRedirServer *s)
     trace_usbredir_server_announce(conn.speed);
     usbredirparser_send_device_connect(s->parser, &conn);
     usbredirparser_do_write(s->parser);
+
+    usbredir_server_send_ep_info(s);
+}
+
+/*
+ * 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);
+    uint8_t value = hdr->value;
+
+    /* IN reports what the device sent, OUT echoes what the host chose. */
+    if (is_in) {
+        value = actual > 0 ? data[0] : 0;
+    }
+
+    switch (reply) {
+    case USBREDIR_SERVER_REPLY_CONTROL:
+        resp.status = status;
+        resp.length = actual;
+        usbredirparser_send_control_packet(s->parser, id, &resp,
+                                           actual > 0 ? data : NULL, actual);
+        break;
+    case USBREDIR_SERVER_REPLY_CONFIG:
+        cfg.status = status;
+        cfg.configuration = value;
+        usbredirparser_send_configuration_status(s->parser, id, &cfg);
+        break;
+    case USBREDIR_SERVER_REPLY_ALT:
+        alt.status = status;
+        alt.alt = 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);
+    bool is_in = !!(rp->ctrl_hdr.requesttype & USB_DIR_IN);
+    int actual = is_in ? rp->pkt.actual_length : 0;
+
+    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_desc(s, rp->data, actual);
+        }
+        break;
+    case DeviceOutRequest | USB_REQ_SET_CONFIGURATION:
+        /* A new configuration puts every interface back on alternate 0. */
+        s->config_value = rp->ctrl_hdr.value;
+        memset(s->alt_setting, 0, sizeof(s->alt_setting));
+        usbredir_server_send_ep_info(s);
+        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_info(s);
+        break;
+    default:
+        break;
+    }
 }
 
 /*
@@ -165,12 +564,20 @@ static void usbredir_server_port_detach(USBPort *port)
 
     trace_usbredir_server_detach(s->device_announced);
 
+    usbredir_server_stop_transfers(s);
+
     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;
+
+    /* Clear the endpoint table: it belongs to the device that is leaving. */
+    usbredir_server_clear_ep_table(s);
+    memset(s->alt_setting, 0, sizeof(s->alt_setting));
+    g_hash_table_remove_all(s->config_descs);
+    s->config_value = 0;
 }
 
 static void usbredir_server_port_child_detach(USBPort *port, USBDevice *child)
@@ -183,20 +590,170 @@ 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);
+}
+
+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);
+}
+
 /*
  * usbredirparser I/O and logging callbacks
  */
@@ -312,9 +869,160 @@ 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);
 }
 
+/*
+ * 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)
+{
+    USBDevice *device = usbredir_server_device(s);
+    USBRedirServerPkt *rp;
+    USBEndpoint *ep0;
+    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);
+    ep0 = usb_ep_get(device, USB_TOKEN_SETUP, 0);
+
+    /* 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);
+    }
+
+    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;
+
+    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->Device, Standard, 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->Host, Standard, 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->Device, Standard, 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->Host, Standard, 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();
@@ -338,6 +1046,80 @@ 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);
+    }
+
+    /*
+     * No "cancelled" response here. This runs on a bus reset, a detach or
+     * a closed chardev, and the host has dropped its own queues already.
+     */
+    while ((rp = QTAILQ_FIRST(&s->inflight)) != NULL) {
+        usbredir_server_pkt_free(s, rp);
+    }
+}
+
 /*
  * Parser setup and teardown
  */
@@ -360,13 +1142,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 +1186,8 @@ static void usbredir_server_destroy_parser(USBRedirServer *s)
 
     g_clear_handle_id(&s->watch, g_source_remove);
 
+    usbredir_server_stop_transfers(s);
+
     if (s->parser) {
         usbredirparser_destroy(s->parser);
         s->parser = NULL;
@@ -480,6 +1270,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,
@@ -487,6 +1278,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_descs = 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);
     usb_register_port(&s->bus, &s->port, s, 0, &usbredir_server_port_ops,
@@ -506,14 +1309,22 @@ 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_descs, g_hash_table_destroy);
+
     if (s->chardev_close_bh) {
         qemu_bh_delete(s->chardev_close_bh);
         s->chardev_close_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[] = {
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index 2cc6a244b9..d63156a930 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -406,3 +406,14 @@ usbredir_server_announce(uint8_t speed) "device_connect speed %u"
 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"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v7 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (4 preceding siblings ...)
  2026-10-28  3:41 ` [PATCH v7 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  2026-10-29 10:09   ` Junjie Cao
  2026-10-28  3:41 ` [PATCH v7 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
  2026-10-28  3:41 ` [PATCH v7 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
  7 siblings, 1 reply; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 hw/usb/redirect-server.c | 239 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |   6 +
 2 files changed, 244 insertions(+), 1 deletion(-)

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 61ac5473e5..7eeb08ecd7 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -90,7 +90,15 @@ 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)
+
 #define USBREDIR_SERVER_CTRL 0
+#define USBREDIR_SERVER_BULK 1
+#define USBREDIR_SERVER_INTERRUPT 2
 
 /* Which message answers the host when a control transfer ends. */
 typedef enum {
@@ -113,7 +121,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.
@@ -546,6 +556,66 @@ 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 len)
+{
+    struct usb_redir_bulk_packet_header resp = *hdr;
+
+    resp.status = status;
+    resp.length = len;
+    resp.length_high = len >> 16;
+
+    usbredirparser_send_bulk_packet(s->parser, id, &resp,
+                                    len > 0 ? data : NULL, 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);
+    bool is_in = !!(rp->bulk_hdr.endpoint & USB_DIR_IN);
+    int actual = rp->pkt.actual_length;
+    /* Only an IN transfer carries data back. */
+    int len = is_in ? actual : 0;
+
+    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, len);
+}
+
+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 len)
+{
+    struct usb_redir_interrupt_packet_header resp = *hdr;
+
+    resp.status = status;
+    resp.length = len;
+
+    usbredirparser_send_interrupt_packet(s->parser, id, &resp,
+                                         len > 0 ? data : NULL, 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);
+    bool is_in = !!(rp->intr_hdr.endpoint & USB_DIR_IN);
+    int actual = rp->pkt.actual_length;
+    /* Only an IN transfer carries data back. */
+    int len = is_in ? actual : 0;
+
+    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, len);
+}
+
 /*
  * USB port ops
  */
@@ -612,6 +682,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();
     }
@@ -1023,6 +1099,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();
@@ -1046,6 +1237,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;
@@ -1086,7 +1295,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:
@@ -1096,6 +1307,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;
     }
@@ -1143,6 +1369,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. */
@@ -1154,12 +1382,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
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index d63156a930..5ec5127efa 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -417,3 +417,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"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v7 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (5 preceding siblings ...)
  2026-10-28  3:41 ` [PATCH v7 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  2026-10-29 17:44   ` marcandre.lureau
  2026-10-28  3:41 ` [PATCH v7 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
  7 siblings, 1 reply; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 hw/usb/redirect-server.c | 298 ++++++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events      |   4 +
 2 files changed, 298 insertions(+), 4 deletions(-)

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 7eeb08ecd7..fecb7119dd 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -96,9 +96,13 @@ 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
+
 #define USBREDIR_SERVER_CTRL 0
 #define USBREDIR_SERVER_BULK 1
 #define USBREDIR_SERVER_INTERRUPT 2
+#define USBREDIR_SERVER_INTR_STREAM 3
 
 /* Which message answers the host when a control transfer ends. */
 typedef enum {
@@ -156,6 +160,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 *stream_timer;
+    QEMUBH *stream_bh;
+    bool streaming;
+
     uint16_t max_packet;
     uint8_t interval;
     uint8_t iface;
@@ -616,6 +629,97 @@ static void usbredir_server_interrupt_complete(USBRedirServer *s,
                                     rp->data, len);
 }
 
+/*
+ * 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->streaming = false;
+        } else {
+            /* the stream stays up, but do not ask at the endpoint's rate */
+            retry = true;
+        }
+        break;
+    }
+
+    if (ep->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->stream_timer,
+                  qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + delay_ns);
+    }
+}
+
 /*
  * USB port ops
  */
@@ -688,6 +792,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();
     }
@@ -727,9 +834,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) +
@@ -775,8 +884,18 @@ 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].streaming) {
+        qemu_bh_schedule(s->ep[idx].stream_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 = {
@@ -830,6 +949,95 @@ static void usbredir_server_pkt_free(USBRedirServer *s, USBRedirServerPkt *rp)
     g_free(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].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;
+    /* streamed data carries no host id */
+    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].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
  */
@@ -1237,6 +1445,62 @@ 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,
+    };
+    USBRedirServer *s = priv;
+    int ep_nr = hdr->endpoint & 0x0f;
+
+    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[usbredir_server_ep_index(hdr->endpoint)].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].streaming = false;
+    qemu_bh_cancel(s->ep[idx].stream_bh);
+    timer_del(s->ep[idx].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)
 {
@@ -1337,6 +1601,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].streaming = false;
+        qemu_bh_cancel(s->ep[i].stream_bh);
+        timer_del(s->ep[i].stream_timer);
+    }
+
     /*
      * No "cancelled" response here. This runs on a bus reset, a detach or
      * a closed chardev, and the host has dropped its own queues already.
@@ -1382,6 +1652,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 =
@@ -1523,6 +1797,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].stream_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+                                             usbredir_server_intr_stream_kick,
+                                             &s->ep[i]);
+        s->ep[i].stream_bh =
+            qemu_bh_new_guarded(usbredir_server_intr_stream_kick, &s->ep[i],
+                                &dev->mem_reentrancy_guard);
+    }
     s->config_descs = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                                             NULL, g_free);
     usbredir_server_clear_ep_table(s);
@@ -1532,7 +1816,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);
 
@@ -1562,6 +1845,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].stream_timer);
+        s->ep[i].stream_timer = NULL;
+        qemu_bh_delete(s->ep[i].stream_bh);
+        s->ep[i].stream_bh = NULL;
+    }
 }
 
 static const Property usbredir_server_props[] = {
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index 5ec5127efa..6eb47085d0 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -423,3 +423,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"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v7 8/8] hw/arm/aspeed: Enable the usbredir server transport
  2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (6 preceding siblings ...)
  2026-10-28  3:41 ` [PATCH v7 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
@ 2026-10-28  3:41 ` Jamin Lin
  7 siblings, 0 replies; 18+ messages in thread
From: Jamin Lin @ 2026-10-28  3:41 UTC (permalink / raw)
  To: kraxel@redhat.com, marcandre.lureau@redhat.com,
	junjie.cao@intel.com, junjie.cao@linux.dev, Cédric Le Goater,
	Peter Maydell, Kane Chen, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

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.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 hw/arm/aspeed.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/hw/arm/aspeed.c b/hw/arm/aspeed.c
index a7cfbd6c5e..fc27ac1788 100644
--- a/hw/arm/aspeed.c
+++ b/hw/arm/aspeed.c
@@ -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;
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub
  2026-10-28  3:41 ` [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub Jamin Lin
@ 2026-10-29 10:09   ` Junjie Cao
  2026-10-29 11:33     ` Gerd Hoffmann
  2026-10-29 17:44   ` marcandre.lureau
  1 sibling, 1 reply; 18+ messages in thread
From: Junjie Cao @ 2026-10-29 10:09 UTC (permalink / raw)
  To: jamin_lin, kraxel, marcandre.lureau
  Cc: clg, peter.maydell, kane_chen, troy_lee, qemu-arm, qemu-devel,
	junjie.cao, junjie.cao

Hi Jamin,

On Wed, Oct 28, 2026 at 03:41:42AM +0000, Jamin Lin wrote:
> xHCI has up to 15 root ports, so it no longer grows a hub on its own. A
> user who wants more ports there adds "-device usb-hub".

qemu-xhci still defaults to 4 ports. With -M pc-q35-11.1, qemu-xhci and
five HID devices, a build without the series puts a hub on port 4 with
the last two behind it. On v7 the fifth device fails with "no free
ports".

Migration from an older QEMU breaks too. Four devices on qemu-xhci, saved
by a build without the series and loaded by v7 with the same command line:

  Unknown section or instance '0000:00:01.0/4/usb-hub' 0

So for the xhci and ehci patches in v8 the flag needs to be a
property that hw_compat_11_1 keeps on for older machine types. For EHCI
that only matters with companions: q35 -usb (ich9 EHCI plus UHCI) puts
the hub on port 6 today.

Thanks,
Junjie


^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device
  2026-10-28  3:41 ` [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
@ 2026-10-29 10:09   ` Junjie Cao
  2026-10-29 17:44   ` marcandre.lureau
  1 sibling, 0 replies; 18+ messages in thread
From: Junjie Cao @ 2026-10-29 10:09 UTC (permalink / raw)
  To: jamin_lin, kraxel, marcandre.lureau
  Cc: clg, peter.maydell, kane_chen, troy_lee, qemu-arm, qemu-devel,
	junjie.cao, junjie.cao

Hi Jamin,

On Wed, Oct 28, 2026 at 03:41:45AM +0000, Jamin Lin wrote:
> +    /* The parser calls these directly, so they must not be NULL. */
> +    s->parser->filter_reject_func = usbredir_server_filter_reject;

start_iso_stream_func, stop_iso_stream_func and iso_packet_func are
still NULL at the end of the series, and the parser accepts those
messages without any capability.

I exported usb-audio to a client QEMU (qemu-xhci, Debian guest) and ran
aplay there. Once the guest picks the streaming alt setting, ep_info
lists the isochronous endpoint, redirect.c sends start_iso_stream, and
the exporting QEMU segfaults on a NULL call in usbredirparser_do_read().

Could the server install these and refuse the stream until it supports
isochronous transfers?

Thanks,
Junjie


^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers
  2026-10-28  3:41 ` [PATCH v7 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
@ 2026-10-29 10:09   ` Junjie Cao
  0 siblings, 0 replies; 18+ messages in thread
From: Junjie Cao @ 2026-10-29 10:09 UTC (permalink / raw)
  To: jamin_lin, kraxel, marcandre.lureau
  Cc: clg, peter.maydell, kane_chen, troy_lee, qemu-arm, qemu-devel,
	junjie.cao, junjie.cao

Hi Jamin,

On Wed, Oct 28, 2026 at 03:41:48AM +0000, Jamin Lin wrote:
> +    int actual = rp->pkt.actual_length;
> +    /* Only an IN transfer carries data back. */
> +    int len = is_in ? actual : 0;

For OUT this also zeroes the length in the reply header, and redirect.c
copies that length into p->actual_length.

With usb-storage exported and qemu-xhci on the client side, the device
enumerates, but the Debian guest keeps resetting it and no sda shows
up. A usb-ehci client works, because EHCI ignores the OUT length.

Keeping actual in the header and sending no payload for OUT, as
usbredirhost does, fixes it here: sda appears on xHCI and a 1 MiB
write/read compares equal. The control and interrupt OUT replies drop
the length the same way.

The endpoint checks from v6 work now, including start_interrupt_receiving
on a bulk endpoint, thanks.

Junjie


^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub
  2026-10-29 10:09   ` Junjie Cao
@ 2026-10-29 11:33     ` Gerd Hoffmann
  2026-10-29 14:49       ` Junjie Cao
  0 siblings, 1 reply; 18+ messages in thread
From: Gerd Hoffmann @ 2026-10-29 11:33 UTC (permalink / raw)
  To: Junjie Cao
  Cc: jamin_lin, marcandre.lureau, clg, peter.maydell, kane_chen,
	troy_lee, qemu-arm, qemu-devel, junjie.cao

On Thu, Oct 29, 2026 at 05:09:40AM -0500, Junjie Cao wrote:
> Hi Jamin,
> 
> On Wed, Oct 28, 2026 at 03:41:42AM +0000, Jamin Lin wrote:
> > xHCI has up to 15 root ports, so it no longer grows a hub on its own. A
> > user who wants more ports there adds "-device usb-hub".
> 
> qemu-xhci still defaults to 4 ports. With -M pc-q35-11.1, qemu-xhci and
> five HID devices, a build without the series puts a hub on port 4 with
> the last two behind it. On v7 the fifth device fails with "no free
> ports".

Which is a bad idea to begin with.  Just configure xhci to have
more ports if 4 is not enough.

> Migration from an older QEMU breaks too.

That is a problem though.

> So for the xhci and ehci patches in v8 the flag needs to be a
> property that hw_compat_11_1 keeps on for older machine types.

Indeed, auto-hub must be a property so we can have different machine
types different defaults (and then only turn it off for new ones).

So, if we need a property anyway, I guess it is better to have all
usb host adapters behave the same and flip auto-hub to default off
for new machines types.  It's easy enough to turn it back on via
qemu command line if needed.

take care,
  Gerd



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub
  2026-10-29 11:33     ` Gerd Hoffmann
@ 2026-10-29 14:49       ` Junjie Cao
  0 siblings, 0 replies; 18+ messages in thread
From: Junjie Cao @ 2026-10-29 14:49 UTC (permalink / raw)
  To: kraxel
  Cc: jamin_lin, marcandre.lureau, clg, peter.maydell, kane_chen,
	troy_lee, qemu-arm, qemu-devel, junjie.cao, junjie.cao

Hi Gerd,

On Thu, Oct 29, 2026 at 12:33:54PM +0100, Gerd Hoffmann wrote:
> Which is a bad idea to begin with.  Just configure xhci to have
> more ports if 4 is not enough.

Indeed, p2=/p3= covers that.

> So, if we need a property anyway, I guess it is better to have all
> usb host adapters behave the same and flip auto-hub to default off
> for new machines types.  It's easy enough to turn it back on via
> qemu command line if needed.

Agreed. One case to account for is raspi: it has no versioned
machine types, and dwc2 has a single root port.
"-device usb-kbd -device usb-mouse" relies on the automatic hub
today; with auto-hub off, the second device would fail with
"no free ports".

I suggest enabling auto-hub in bcm2835_peripherals to preserve
the current board behaviour while leaving the controller default
off.

I will rerun the migration check on v8.

Thanks,
Junjie


^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub
  2026-10-28  3:41 ` [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub Jamin Lin
  2026-10-29 10:09   ` Junjie Cao
@ 2026-10-29 17:44   ` marcandre.lureau
  1 sibling, 0 replies; 18+ messages in thread
From: marcandre.lureau @ 2026-10-29 17:44 UTC (permalink / raw)
  To: Jamin Lin
  Cc: kraxel, marcandre.lureau, junjie.cao, junjie.cao,
	Cédric Le Goater, Peter Maydell, Kane Chen,
	open list:ASPEED BMCs, open list:All patches CC here, Troy Lee

> usb_claim_port() plugs in a usb-hub when only one port is free, so that
> more devices fit. Every bus gets that today. A controller with a single
> root port then puts its only device behind a hub, and a transport that
> carries one device cannot use that.
> 
> The hub speaks USB 1.1, so this belongs to UHCI and OHCI. Add auto_hub
> and leave it off by default. UHCI and OHCI set it, both when they own
> the bus and when they join one as a companion. dwc2 sets it too, so the
> boards that use it keep working.
> 
> xHCI has up to 15 root ports, so it no longer grows a hub on its own. A
> user who wants more ports there adds "-device usb-hub".
> 
> Suggested-by: Gerd Hoffmann <kraxel@redhat.com>
> Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> Message-ID: <20261028034140.367393-2-jamin_lin@aspeedtech.com>
> ---
>  include/hw/usb/usb.h | 1 +
>  hw/usb/bus.c         | 6 +++++-
>  hw/usb/hcd-dwc2.c    | 1 +
>  hw/usb/hcd-ohci.c    | 1 +
>  hw/usb/hcd-uhci.c    | 1 +
>  5 files changed, 9 insertions(+), 1 deletion(-)
>
> diff --git a/hw/usb/bus.c b/hw/usb/bus.c
> index 5cc5ffec33a1..d1ad22ac9432 100644
> --- a/hw/usb/bus.c
> +++ b/hw/usb/bus.c
> @@ -363,6 +363,9 @@ void usb_register_companion(const char *masterbus, USBPort *ports[],
>          return;
>      }
>  
> +    /* Only UHCI and OHCI come in as a companion, and both want the hub. */
> +    bus->auto_hub = true;
> +
>      for (i = 0; i < portcount; i++) {
>          usb_fill_port(ports[i], opaque, i, ops, speedmask);
>      }
> @@ -413,7 +416,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) {
> diff --git a/hw/usb/hcd-dwc2.c b/hw/usb/hcd-dwc2.c
> index cd3b61158d17..104f49310502 100644
> --- a/hw/usb/hcd-dwc2.c
> +++ b/hw/usb/hcd-dwc2.c
> @@ -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 = true;
>      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));
> diff --git a/hw/usb/hcd-ohci.c b/hw/usb/hcd-ohci.c
> index 5e5fe41f473d..d6b1fc5b62ae 100644
> --- a/hw/usb/hcd-ohci.c
> +++ b/hw/usb/hcd-ohci.c
> @@ -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 = true;
>          for (i = 0; i < num_ports; i++) {
>              usb_register_port(&ohci->bus, &ohci->rhport[i].port,
>                                ohci, i, &ohci_port_ops,
> diff --git a/hw/usb/hcd-uhci.c b/hw/usb/hcd-uhci.c
> index a7b9fe131759..f11664bd553c 100644
> --- a/hw/usb/hcd-uhci.c
> +++ b/hw/usb/hcd-uhci.c
> @@ -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 = true;
>          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);
> diff --git a/include/hw/usb/usb.h b/include/hw/usb/usb.h
> index cbd4711933e8..cc1d6d74b124 100644
> --- a/include/hw/usb/usb.h
> +++ b/include/hw/usb/usb.h
> @@ -484,6 +484,7 @@ struct USBBus {
>      int busnr;
>      int nfree;
>      int nused;
> +    bool auto_hub;

Missing xen-usb, I assume it will be taken care of in v8.

-- 
Marc-André Lureau <marcandre.lureau@redhat.com>



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints
  2026-10-28  3:41 ` [PATCH v7 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
@ 2026-10-29 17:44   ` marcandre.lureau
  0 siblings, 0 replies; 18+ messages in thread
From: marcandre.lureau @ 2026-10-29 17:44 UTC (permalink / raw)
  To: Jamin Lin
  Cc: kraxel, marcandre.lureau, junjie.cao, junjie.cao,
	Cédric Le Goater, Peter Maydell, Kane Chen,
	open list:ASPEED BMCs, open list:All patches CC here, Troy Lee

> 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.
> 
> Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> Message-ID: <20261028034140.367393-8-jamin_lin@aspeedtech.com>
> ---
>  hw/usb/redirect-server.c | 298 ++++++++++++++++++++++++++++++++++++++-
>  hw/usb/trace-events      |   4 +
>  2 files changed, 298 insertions(+), 4 deletions(-)
>
> diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
> index 7eeb08ecd7f1..fecb7119ddd4 100644
> --- a/hw/usb/redirect-server.c
> +++ b/hw/usb/redirect-server.c
> @@ -96,9 +96,13 @@ 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
> +
>  #define USBREDIR_SERVER_CTRL 0
>  #define USBREDIR_SERVER_BULK 1
>  #define USBREDIR_SERVER_INTERRUPT 2
> +#define USBREDIR_SERVER_INTR_STREAM 3
>  
>  /* Which message answers the host when a control transfer ends. */
>  typedef enum {
> @@ -156,6 +160,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 *stream_timer;
> +    QEMUBH *stream_bh;
> +    bool streaming;
> +
>      uint16_t max_packet;
>      uint8_t interval;
>      uint8_t iface;
> @@ -616,6 +629,97 @@ static void usbredir_server_interrupt_complete(USBRedirServer *s,
>                                      rp->data, len);
>  }
>  
> +/*
> + * 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->streaming = false;
> +        } else {
> +            /* the stream stays up, but do not ask at the endpoint's rate */
> +            retry = true;
> +        }
> +        break;
> +    }
> +
> +    if (ep->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->stream_timer,
> +                  qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + delay_ns);
> +    }
> +}
> +
>  /*
>   * USB port ops
>   */
> @@ -688,6 +792,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();
>      }
> @@ -727,9 +834,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) +
> @@ -775,8 +884,18 @@ 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);
>  
> -    usbredir_server_kick_ep(s, 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].streaming) {
> +        qemu_bh_schedule(s->ep[idx].stream_bh);
> +    }
> +
> +    /* The host may also have a request stopped on this endpoint. */
> +    usbredir_server_kick_ep(s, idx);
>  }
>  
>  static USBBusOps usbredir_server_bus_ops = {
> @@ -830,6 +949,95 @@ static void usbredir_server_pkt_free(USBRedirServer *s, USBRedirServerPkt *rp)
>      g_free(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].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;
> +    /* streamed data carries no host id */
> +    rp->redir_id = 0;

This puts a packet with id 0 on s->inflight, but
usbredir_server_cancel_data_packet() matches any inflight request by id.

A client cancel for a packet id == 0 can therefore hit this packet, send
a bogus cancelled interrupt packet. Skip USBREDIR_SERVER_INTR_STREAM in
the cancel lookup?

-- 
Marc-André Lureau <marcandre.lureau@redhat.com>



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device
  2026-10-28  3:41 ` [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
  2026-10-29 10:09   ` Junjie Cao
@ 2026-10-29 17:44   ` marcandre.lureau
  1 sibling, 0 replies; 18+ messages in thread
From: marcandre.lureau @ 2026-10-29 17:44 UTC (permalink / raw)
  To: Jamin Lin
  Cc: kraxel, marcandre.lureau, junjie.cao, junjie.cao,
	Cédric Le Goater, Peter Maydell, Kane Chen,
	open list:ASPEED BMCs, open list:All patches CC here, Troy Lee

> 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.
> 
> Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Message-ID: <20261028034140.367393-5-jamin_lin@aspeedtech.com>
> ---
>  hw/usb/redirect-server.c | 130 ++++++++++++++++++++++++++++++++++++++-
>  hw/usb/trace-events      |   7 +++
>  2 files changed, 136 insertions(+), 1 deletion(-)
>
> diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
> index 4971d81af489..ad6e4c422bb9 100644
> --- a/hw/usb/redirect-server.c
> +++ b/hw/usb/redirect-server.c
> @@ -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"
> @@ -90,12 +91,88 @@ struct USBRedirServer {
>      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_device(USBRedirServer *s)
> +{
> +    USBDevice *device = usbredir_server_device(s);
> +    struct usb_redir_interface_info_header iface_info = {
> +        .interface_count = 0,
> +    };
> +    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);
> +
> +    /* Put the device in DEFAULT state. The host may not reset the bus. */
> +    device->addr = 0;
> +    device->state = USB_STATE_DEFAULT;

usb_attach() overwrites this with USB_STATE_ATTACHED right after the
port attach callback returns. A subsequent control request from the peer
would trip the dev->state == USB_STATE_DEFAULT assertion in
usb_handle_packet().

You could announce with a BH instead (cancel it on detach) and use
usb_device_reset() instead of manually poking the fields before
announcing.

-- 
Marc-André Lureau <marcandre.lureau@redhat.com>



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v7 5/8] hw/usb/redirect-server: Implement control transfers
  2026-10-28  3:41 ` [PATCH v7 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
@ 2026-10-29 17:44   ` marcandre.lureau
  0 siblings, 0 replies; 18+ messages in thread
From: marcandre.lureau @ 2026-10-29 17:44 UTC (permalink / raw)
  To: Jamin Lin
  Cc: kraxel, marcandre.lureau, junjie.cao, junjie.cao,
	Cédric Le Goater, Peter Maydell, Kane Chen,
	open list:ASPEED BMCs, open list:All patches CC here, Troy Lee

Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>

-- 
Marc-André Lureau <marcandre.lureau@redhat.com>



^ permalink raw reply	[flat|nested] 18+ messages in thread

end of thread, other threads:[~2026-10-29 17:55 UTC | newest]

Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-28  3:41 [PATCH v7 0/8] hw/usb: Add a usbredir server transport Jamin Lin
2026-10-28  3:41 ` [PATCH v7 1/8] hw/usb/bus: Let the controller ask for the automatic hub Jamin Lin
2026-10-29 10:09   ` Junjie Cao
2026-10-29 11:33     ` Gerd Hoffmann
2026-10-29 14:49       ` Junjie Cao
2026-10-29 17:44   ` marcandre.lureau
2026-10-28  3:41 ` [PATCH v7 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
2026-10-28  3:41 ` [PATCH v7 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
2026-10-28  3:41 ` [PATCH v7 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
2026-10-29 10:09   ` Junjie Cao
2026-10-29 17:44   ` marcandre.lureau
2026-10-28  3:41 ` [PATCH v7 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
2026-10-29 17:44   ` marcandre.lureau
2026-10-28  3:41 ` [PATCH v7 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
2026-10-29 10:09   ` Junjie Cao
2026-10-28  3:41 ` [PATCH v7 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
2026-10-29 17:44   ` marcandre.lureau
2026-10-28  3:41 ` [PATCH v7 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.