All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 0/8] hw/usb: Add a usbredir server transport
@ 2026-10-12  2:16 Jamin Lin
  2026-10-12  2:16 ` [PATCH v3 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
                   ` (8 more replies)
  0 siblings, 9 replies; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, 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.


Jamin Lin (8):
  hw/usb/bus: Let a bus opt out of automatic hub insertion
  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             |    3 +-
 hw/usb/redirect-server.c | 1669 ++++++++++++++++++++++++++++++++++++++
 hw/usb/meson.build       |    3 +-
 hw/usb/trace-events      |   27 +
 6 files changed, 1708 insertions(+), 2 deletions(-)
 create mode 100644 hw/usb/redirect-server.c

-- 
2.53.0


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

* [PATCH v3 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12  2:16 ` [PATCH v3 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee, Marc-André Lureau

usb_claim_port() inserts a usb-hub when a device is plugged in and only
one port is still free, so that more devices can follow. That is what a
host controller with root ports wants.

It is wrong for a transport that carries exactly one device. The hub
takes the last port, and the device that was being plugged in ends up
behind the hub instead of on the port the controller watches.

Add no_auto_hub for such a bus. It keeps the ports it registered, and
-device fails with "no free ports" once they are used up.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/hw/usb/usb.h | 1 +
 hw/usb/bus.c         | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/include/hw/usb/usb.h b/include/hw/usb/usb.h
index cbd4711933..ab96812adb 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 no_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..8a24b1837a 100644
--- a/hw/usb/bus.c
+++ b/hw/usb/bus.c
@@ -413,7 +413,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->no_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) {
-- 
2.53.0


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

* [PATCH v3 2/8] hw/usb/redirect-server: Add a usbredir server device
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
  2026-10-12  2:16 ` [PATCH v3 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12 10:01   ` marcandre.lureau
  2026-10-12  2:16 ` [PATCH v3 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
                   ` (6 subsequent siblings)
  8 siblings, 1 reply; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, 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>
---
 hw/usb/redirect-server.c | 143 +++++++++++++++++++++++++++++++++++++++
 hw/usb/meson.build       |   3 +-
 2 files changed, 145 insertions(+), 1 deletion(-)
 create mode 100644 hw/usb/redirect-server.c

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
new file mode 100644
index 0000000000..bc883add0e
--- /dev/null
+++ b/hw/usb/redirect-server.c
@@ -0,0 +1,143 @@
+/*
+ * USB redirector, server side
+ *
+ * Copyright (c) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Exports a locally emulated USB device to a remote USB host over the
+ * usbredir protocol, so a device emulated in one QEMU instance can be
+ * enumerated by a host controller emulated in another one.
+ *
+ * Architecture. The left column is a request going to the device. The
+ * right column is the answer coming back. The middle hop carries
+ * usbredir messages over a socket. The top and bottom hops carry
+ * USBPackets inside QEMU.
+ *
+ *     remote QEMU: guest driver -> EHCI/XHCI
+ *              |                     ^
+ *    USBPacket |                     | USBPacket
+ *              v                     |
+ *     "usb-redir" (the client)
+ *              |                     ^
+ *     usbredir |   chardev socket    | usbredir
+ *              v                     |
+ *     x-usb-redir-server (the server, this file)
+ *              |                     ^
+ *    USBPacket |                     | USBPacket
+ *              v                     |
+ *     any USBDevice, "-device <dev>,bus=<id>.0"
+ *
+ * "usb-redir" (hw/usb/redirect.c) is the client:
+ *   - it takes a USBPacket from the remote guest and writes it to the
+ *     socket as a usbredir message
+ *   - it reads the answer from the socket and completes the USBPacket
+ *
+ * This file is the server. It does the same thing, but backwards:
+ *   - it reads a usbredir message from the socket and runs it as a
+ *     USBPacket on the bus below
+ *   - it takes the result of that USBPacket and writes it back to the
+ *     same socket as a usbredir message, for the client to read
+ *
+ * A USB device has to sit on a USB bus, and in QEMU a USB bus is always
+ * made by a host controller. So this file makes one and acts as the
+ * host controller on this side. It models no real chip: its cable is
+ * the chardev socket. The real host is in the other QEMU.
+ *
+ * usbredir carries one device, not a bus. A hub cannot be exported:
+ * the protocol has no device address field. To export several devices,
+ * run one x-usb-redir-server per device, each with its own chardev.
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qemu/module.h"
+#include "migration/vmstate.h"
+#include "hw/core/sysbus.h"
+#include "hw/usb/usb.h"
+#include "qom/object.h"
+
+#define TYPE_USB_REDIR_SERVER "x-usb-redir-server"
+OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
+
+struct USBRedirServer {
+    SysBusDevice parent_obj;
+
+    /* USB bus */
+    USBBus bus;
+    USBPort port;
+};
+
+/*
+ * USB port ops
+ */
+
+static void usbredir_server_port_child_detach(USBPort *port, USBDevice *child)
+{
+    /* We only export the device on our own port. Nothing to do. */
+}
+
+static void usbredir_server_port_wakeup(USBPort *port)
+{
+    /* We do not pass remote wakeup to the host. Nothing to do. */
+}
+
+static USBPortOps usbredir_server_port_ops = {
+    .child_detach = usbredir_server_port_child_detach,
+    .wakeup = usbredir_server_port_wakeup,
+};
+
+/*
+ * USB bus ops
+ */
+
+static USBBusOps usbredir_server_bus_ops = {
+};
+
+/*
+ * Device registration
+ */
+
+static void usbredir_server_realize(DeviceState *dev, Error **errp)
+{
+    USBRedirServer *s = USB_REDIR_SERVER(dev);
+
+    /* One port: usbredir carries a single device. */
+    usb_bus_new(&s->bus, sizeof(s->bus), &usbredir_server_bus_ops, dev);
+    s->bus.no_auto_hub = true;
+    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] 17+ messages in thread

* [PATCH v3 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
  2026-10-12  2:16 ` [PATCH v3 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
  2026-10-12  2:16 ` [PATCH v3 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12  2:16 ` [PATCH v3 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee, Marc-André Lureau

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 | 284 +++++++++++++++++++++++++++++++++++++++
 hw/usb/trace-events      |   6 +
 2 files changed, 290 insertions(+)

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index bc883add0e..a48b821642 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,232 @@ 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:
+        break;
+    }
+}
+
 /*
  * Device registration
  */
@@ -102,14 +352,46 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
 {
     USBRedirServer *s = USB_REDIR_SERVER(dev);
 
+    if (!qemu_chr_fe_backend_connected(&s->cs)) {
+        error_setg(errp,
+                   TYPE_USB_REDIR_SERVER ": 'chardev' property must be set");
+        return;
+    }
+
     /* One port: usbredir carries a single device. */
     usb_bus_new(&s->bus, sizeof(s->bus), &usbredir_server_bus_ops, dev);
     s->bus.no_auto_hub = true;
     usb_register_port(&s->bus, &s->port, s, 0, &usbredir_server_port_ops,
                       USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL |
                       USB_SPEED_MASK_HIGH);
+
+    s->chardev_close_bh = qemu_bh_new_guarded(usbredir_server_chardev_close_bh,
+                                              s, &dev->mem_reentrancy_guard);
+
+    qemu_chr_fe_set_handlers(&s->cs,
+                             usbredir_server_chardev_can_read,
+                             usbredir_server_chardev_read,
+                             usbredir_server_chardev_event,
+                             NULL, s, NULL, true);
+}
+
+static void usbredir_server_unrealize(DeviceState *dev)
+{
+    USBRedirServer *s = USB_REDIR_SERVER(dev);
+
+    qemu_chr_fe_deinit(&s->cs, true);
+    usbredir_server_destroy_parser(s);
+
+    if (s->chardev_close_bh) {
+        qemu_bh_delete(s->chardev_close_bh);
+        s->chardev_close_bh = NULL;
+    }
 }
 
+static const Property usbredir_server_props[] = {
+    DEFINE_PROP_CHR("chardev", USBRedirServer, cs),
+};
+
 /*
  * The link to the remote host is a chardev, and we cannot migrate
  * that. So this device cannot be migrated either.
@@ -125,8 +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] 17+ messages in thread

* [PATCH v3 4/8] hw/usb/redirect-server: Announce the exported device
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (2 preceding siblings ...)
  2026-10-12  2:16 ` [PATCH v3 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12 10:01   ` marcandre.lureau
  2026-10-12  2:16 ` [PATCH v3 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
                   ` (4 subsequent siblings)
  8 siblings, 1 reply; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, 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>
---
 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 a48b821642..05ee0f3873 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] 17+ messages in thread

* [PATCH v3 5/8] hw/usb/redirect-server: Implement control transfers
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (3 preceding siblings ...)
  2026-10-12  2:16 ` [PATCH v3 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12 10:01   ` marcandre.lureau
  2026-10-12  2:16 ` [PATCH v3 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
                   ` (3 subsequent siblings)
  8 siblings, 1 reply; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

The host sends a control_packet. Turn it into the SETUP, DATA and
STATUS tokens the QEMU USB core wants, run them on the device, and
send the answer back.

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.

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:
bulk, interrupt or control. 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.

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

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 05ee0f3873..22083394ab 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -63,9 +63,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 +75,41 @@ 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
+
+#define USBREDIR_SERVER_CTRL_SETUP 0
+#define USBREDIR_SERVER_CTRL_STATUS 1
+
+/* 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;
+
+typedef struct USBRedirServerPkt {
+    USBPacket pkt;
+
+    /* Saved headers 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; data_size is how big it is.
+     */
+    uint8_t *data;
+    int data_size;
+    QTAILQ_ENTRY(USBRedirServerPkt) next;
+    USBRedirServerReply reply;
+    uint64_t redir_id;
+    int type;
+} USBRedirServerPkt;
+
 struct USBRedirServer {
     SysBusDevice parent_obj;
 
@@ -92,8 +129,23 @@ struct USBRedirServer {
     guint watch;
     bool host_connected;
     bool device_announced;
+
+    /* In-flight packet tracking */
+    QTAILQ_HEAD(, USBRedirServerPkt) inflight;
+    uint64_t next_id;
+
+    /* Endpoint tables for the usbredir ep_info message. */
+    uint8_t ep_type[USBREDIR_SERVER_MAX_EP];
+    uint16_t ep_max_packet[USBREDIR_SERVER_MAX_EP];
+    uint8_t ep_interval[USBREDIR_SERVER_MAX_EP];
+    uint8_t ep_iface[USBREDIR_SERVER_MAX_EP];
 };
 
+static void usbredir_server_pkt_free(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 +156,98 @@ static USBDevice *usbredir_server_device(USBRedirServer *s)
 }
 
 /*
- * Device announcement
+ * Descriptor snooping and device announcement
  */
 
+static void usbredir_server_record_endpoint(USBRedirServer *s,
+                                            const USBDescriptor *desc,
+                                            uint8_t iface)
+{
+    uint8_t addr;
+    int ep_nr;
+    int idx;
+
+    if (desc->bLength < 7) {
+        return;
+    }
+
+    addr = desc->u.endpoint.bEndpointAddress;
+    ep_nr = addr & 0x0f;
+    idx = (addr & USB_DIR_IN) ? ep_nr + USBREDIR_SERVER_EP_IN_BASE : ep_nr;
+    if (ep_nr < 1 || idx >= USBREDIR_SERVER_MAX_EP) {
+        return;
+    }
+
+    s->ep_type[idx] = desc->u.endpoint.bmAttributes & 0x03;
+    s->ep_max_packet[idx] = (desc->u.endpoint.wMaxPacketSize_hi << 8) |
+                            desc->u.endpoint.wMaxPacketSize_lo;
+    s->ep_interval[idx] = desc->u.endpoint.bInterval;
+    s->ep_iface[idx] = iface;
+}
+
+/*
+ * A device that only passes transfers through never gets its endpoint
+ * types filled in: they stay INVALID and there is nothing to put in
+ * ep_info. Take them from the configuration descriptor as it goes past.
+ * Without them the host sees type 255 and refuses to move data.
+ */
+static void usbredir_server_snoop_config_desc(USBRedirServer *s,
+                                              const uint8_t *data, int len)
+{
+    const USBDescriptor *desc;
+    uint8_t iface = 0;
+    int i = 0;
+
+    while (i + 2 <= len) {
+        desc = (const USBDescriptor *)(data + i);
+
+        if (desc->bLength < 2 || i + desc->bLength > len) {
+            break;
+        }
+
+        if (desc->bDescriptorType == USB_DT_INTERFACE && desc->bLength >= 3) {
+            iface = desc->u.interface.bInterfaceNumber;
+        } else if (desc->bDescriptorType == USB_DT_ENDPOINT) {
+            usbredir_server_record_endpoint(s, desc, iface);
+        }
+
+        i += desc->bLength;
+    }
+}
+
+static void usbredir_server_send_ep_info(USBRedirServer *s)
+{
+    struct usb_redir_ep_info_header ep_info = {};
+    int i;
+
+    for (i = 0; i < USBREDIR_SERVER_MAX_EP; i++) {
+        ep_info.type[i] = s->ep_type[i];
+        ep_info.max_packet_size[i] = s->ep_max_packet[i];
+        ep_info.interface[i] = s->ep_iface[i];
+        /*
+         * 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 send 1 instead.
+         */
+        ep_info.interval[i] = s->ep_interval[i];
+        if (ep_info.interval[i] == 0 &&
+            (s->ep_type[i] == USB_ENDPOINT_XFER_INT ||
+             s->ep_type[i] == USB_ENDPOINT_XFER_ISOC)) {
+            ep_info.interval[i] = 1;
+        }
+        if (s->ep_type[i] != 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 +286,146 @@ 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;
+    }
+}
+
+/*
+ * The device answered the setup token of an IN control request. What it
+ * wants to send is now in device->data_buf, so run the data and status
+ * stages and pass the answer to the host.
+ *
+ * Each stage reuses rp->pkt, and usb_packet_setup() asserts iov->iov is
+ * not NULL, so call qemu_iovec_init() before every stage.
+ */
+static void usbredir_server_ctrl_setup_complete(USBRedirServer *s,
+                                                USBRedirServerPkt *rp)
+{
+    struct usb_redir_control_packet_header resp = rp->ctrl_hdr;
+    struct usb_redir_configuration_status_header cfg = {};
+    struct usb_redir_alt_setting_status_header alt = {
+        .interface = rp->ctrl_hdr.index,
+    };
+    USBDevice *device = usbredir_server_device(s);
+    USBEndpoint *ep_out = usb_ep_get(device, USB_TOKEN_OUT, 0);
+    USBEndpoint *ep_in = usb_ep_get(device, USB_TOKEN_IN, 0);
+    uint8_t status = usbredir_server_status(rp->pkt.status);
+    int actual = 0;
+
+    if (rp->pkt.status == USB_RET_SUCCESS) {
+        /* Data stage: the core copies device->data_buf into our buffer. */
+        qemu_iovec_init(&rp->pkt.iov, 1);
+        usb_packet_setup(&rp->pkt, USB_TOKEN_IN, ep_in,
+                         0, s->next_id++, false, false);
+        usb_packet_addbuf(&rp->pkt, rp->data, rp->data_size);
+        usb_handle_packet(device, &rp->pkt);
+        actual = rp->pkt.actual_length;
+        usb_packet_cleanup(&rp->pkt);
+
+        /* Status stage: tell the device we got it. Its EP0 goes idle. */
+        qemu_iovec_init(&rp->pkt.iov, 1);
+        usb_packet_setup(&rp->pkt, USB_TOKEN_OUT, ep_out,
+                         0, s->next_id++, false, false);
+        usb_handle_packet(device, &rp->pkt);
+        usb_packet_cleanup(&rp->pkt);
+
+        if (rp->ctrl_hdr.request == USB_REQ_GET_DESCRIPTOR &&
+            (rp->ctrl_hdr.value >> 8) == USB_DT_CONFIG && actual > 0) {
+            usbredir_server_snoop_config_desc(s, rp->data, actual);
+        }
+    }
+
+    trace_usbredir_server_ctrl_setup_complete(rp->redir_id, status,
+                                              actual);
+
+    switch (rp->reply) {
+    case USBREDIR_SERVER_REPLY_CONTROL:
+        resp.status = status;
+        resp.length = actual;
+        usbredirparser_send_control_packet(s->parser, rp->redir_id, &resp,
+                                           actual > 0 ? rp->data : NULL,
+                                           actual);
+        break;
+    case USBREDIR_SERVER_REPLY_CONFIG:
+        cfg.status = status;
+        cfg.configuration = actual > 0 ? rp->data[0] : 0;
+        usbredirparser_send_configuration_status(s->parser, rp->redir_id,
+                                                 &cfg);
+        break;
+    case USBREDIR_SERVER_REPLY_ALT:
+        alt.status = status;
+        alt.alt = actual > 0 ? rp->data[0] : 0;
+        usbredirparser_send_alt_setting_status(s->parser, rp->redir_id,
+                                               &alt);
+        break;
+    }
+    usbredirparser_do_write(s->parser);
+}
+
+/*
+ * The status stage of an OUT control request finished, so the device is
+ * done. Tell the host how it went, with whichever message it is waiting
+ * for.
+ */
+static void usbredir_server_ctrl_status_complete(USBRedirServer *s,
+                                                 USBRedirServerPkt *rp)
+{
+    struct usb_redir_control_packet_header resp = rp->ctrl_hdr;
+    uint8_t status = usbredir_server_status(rp->pkt.status);
+    struct usb_redir_configuration_status_header cfg = {
+        .configuration = rp->ctrl_hdr.value,
+    };
+    struct usb_redir_alt_setting_status_header alt = {
+        .interface = rp->ctrl_hdr.index,
+        .alt = rp->ctrl_hdr.value,
+    };
+
+    trace_usbredir_server_ctrl_status_complete(rp->redir_id, status);
+
+    switch (rp->reply) {
+    case USBREDIR_SERVER_REPLY_CONTROL:
+        resp.status = status;
+        resp.length = 0;
+        usbredirparser_send_control_packet(s->parser, rp->redir_id,
+                                           &resp, NULL, 0);
+        break;
+    case USBREDIR_SERVER_REPLY_CONFIG:
+        cfg.status = status;
+        usbredirparser_send_configuration_status(s->parser, rp->redir_id,
+                                                 &cfg);
+        break;
+    case USBREDIR_SERVER_REPLY_ALT:
+        alt.status = status;
+        usbredirparser_send_alt_setting_status(s->parser, rp->redir_id,
+                                               &alt);
+        break;
+    }
+    usbredirparser_do_write(s->parser);
+
+    /* The endpoint list changed. Send the new one. */
+    if (status == usb_redir_success &&
+        (rp->ctrl_hdr.request == USB_REQ_SET_CONFIGURATION ||
+         rp->ctrl_hdr.request == USB_REQ_SET_INTERFACE)) {
+        usbredir_server_send_ep_info(s);
+    }
 }
 
 /*
@@ -165,12 +446,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 tables: they belong to the device that is leaving. */
+    memset(s->ep_type, USB_ENDPOINT_XFER_INVALID, sizeof(s->ep_type));
+    memset(s->ep_max_packet, 0, sizeof(s->ep_max_packet));
+    memset(s->ep_interval, 0, sizeof(s->ep_interval));
+    memset(s->ep_iface, 0, sizeof(s->ep_iface));
 }
 
 static void usbredir_server_port_child_detach(USBPort *port, USBDevice *child)
@@ -183,11 +472,42 @@ 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_submit_to_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);
+
+    QTAILQ_REMOVE(&s->inflight, rp, next);
+    usb_packet_cleanup(&rp->pkt);
+
+    if (!s->parser || !device) {
+        usbredir_server_pkt_free(rp);
+        return;
+    }
+
+    switch (rp->type) {
+    case USBREDIR_SERVER_CTRL_SETUP:
+        usbredir_server_ctrl_setup_complete(s, rp);
+        break;
+    case USBREDIR_SERVER_CTRL_STATUS:
+        usbredir_server_ctrl_status_complete(s, rp);
+        break;
+    }
+
+    usbredir_server_pkt_free(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,
 };
 
 /*
@@ -197,6 +517,54 @@ static USBPortOps usbredir_server_port_ops = {
 static USBBusOps usbredir_server_bus_ops = {
 };
 
+/*
+ * Submit a packet to the device. The core only calls our completion
+ * callback when the device answers USB_RET_ASYNC, so call it here for
+ * the rest.
+ */
+static void usbredir_server_submit_to_device(USBRedirServer *s,
+                                             USBRedirServerPkt *rp)
+{
+    QTAILQ_INSERT_TAIL(&s->inflight, rp, next);
+    usb_handle_packet(usbredir_server_device(s), &rp->pkt);
+    if (rp->pkt.status != USB_RET_ASYNC) {
+        usbredir_server_packet_complete(&s->port, &rp->pkt);
+    }
+}
+
+static USBRedirServerPkt *usbredir_server_pkt_alloc(int size)
+{
+    USBRedirServerPkt *rp = g_new0(USBRedirServerPkt, 1);
+
+    qemu_iovec_init(&rp->pkt.iov, 1);
+    rp->data = g_malloc0(size);
+    rp->data_size = size;
+    return rp;
+}
+
+static void usbredir_server_pkt_free(USBRedirServerPkt *rp)
+{
+    g_free(rp->data);
+    g_free(rp);
+}
+
+/*
+ * Take @rp off the list and free it. Do not use usb_packet_complete():
+ * it calls back into usbredir_server_packet_complete(), which would
+ * remove the entry a second time and free it. usb_cancel_packet() only
+ * tells the device to let go.
+ */
+static void usbredir_server_drop_pkt(USBRedirServer *s,
+                                     USBRedirServerPkt *rp)
+{
+    QTAILQ_REMOVE(&s->inflight, rp, next);
+    if (usb_packet_is_inflight(&rp->pkt)) {
+        usb_cancel_packet(&rp->pkt);
+    }
+    usb_packet_cleanup(&rp->pkt);
+    usbredir_server_pkt_free(rp);
+}
+
 /*
  * usbredirparser I/O and logging callbacks
  */
@@ -312,9 +680,199 @@ 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 *ep_out;
+    USBEndpoint *ep_in;
+    USBEndpoint *ep0;
+    bool is_in;
+    int size;
+
+    if (!s->host_connected || !device || !device->attached) {
+        return;
+    }
+
+    trace_usbredir_server_control(id, hdr->requesttype, hdr->request,
+                                  hdr->value, hdr->index, hdr->length);
+
+    is_in = !!(hdr->requesttype & USB_DIR_IN);
+
+    ep0 = usb_ep_get(device, USB_TOKEN_SETUP, 0);
+
+    /* Room for the setup bytes and for the data of either direction. */
+    size = MAX(hdr->length, data_len);
+    size = MAX(size, (int)sizeof(device->setup_buf));
+
+    rp = usbredir_server_pkt_alloc(size);
+    rp->redir_id = id;
+    rp->reply = reply;
+    rp->ctrl_hdr = *hdr;
+
+    /*
+     * Build the raw 8-byte SETUP packet in rp->data. The IN path
+     * overwrites it later with the answer from the device.
+     */
+    rp->data[0] = hdr->requesttype;
+    rp->data[1] = hdr->request;
+    rp->data[2] = hdr->value & 0xff;
+    rp->data[3] = (hdr->value >> 8) & 0xff;
+    rp->data[4] = hdr->index & 0xff;
+    rp->data[5] = (hdr->index >> 8) & 0xff;
+    rp->data[6] = hdr->length & 0xff;
+    rp->data[7] = (hdr->length >> 8) & 0xff;
+
+    if (is_in) {
+        /*
+         * An IN request. The device starts the work on the setup token
+         * and may take its time, so send the token and pick the rest up
+         * in usbredir_server_ctrl_setup_complete().
+         */
+        rp->type = USBREDIR_SERVER_CTRL_SETUP;
+        usb_packet_setup(&rp->pkt, USB_TOKEN_SETUP, ep0,
+                         0, s->next_id++, false, false);
+        usb_packet_addbuf(&rp->pkt, rp->data, sizeof(device->setup_buf));
+        usbredir_server_submit_to_device(s, rp);
+    } else {
+        /*
+         * An OUT request. The device only stores the setup bytes now and
+         * does the work on the status stage, so run the first two stages
+         * here and wait on the last one.
+         */
+        ep_in = usb_ep_get(device, USB_TOKEN_IN, 0);
+        ep_out = usb_ep_get(device, USB_TOKEN_OUT, 0);
+
+        /* Setup stage: hand the device the 8 setup bytes. */
+        usb_packet_setup(&rp->pkt, USB_TOKEN_SETUP, ep0,
+                         0, s->next_id++, false, false);
+        usb_packet_addbuf(&rp->pkt, rp->data, sizeof(device->setup_buf));
+        usb_handle_packet(device, &rp->pkt);
+        usb_packet_cleanup(&rp->pkt);
+
+        /* Data stage: send the bytes that came with the request. */
+        if (data_len > 0) {
+            memcpy(rp->data, data, data_len);
+            qemu_iovec_init(&rp->pkt.iov, 1);
+            usb_packet_setup(&rp->pkt, USB_TOKEN_OUT, ep_out,
+                             0, s->next_id++, false, false);
+            usb_packet_addbuf(&rp->pkt, rp->data, data_len);
+            /* The core copies our bytes into device->data_buf. */
+            usb_handle_packet(device, &rp->pkt);
+            usb_packet_cleanup(&rp->pkt);
+        }
+
+        /* Status stage: the device does the work here, so wait for it. */
+        rp->type = USBREDIR_SERVER_CTRL_STATUS;
+        qemu_iovec_init(&rp->pkt.iov, 1);
+        usb_packet_setup(&rp->pkt, USB_TOKEN_IN, ep_in,
+                         0, s->next_id++, false, false);
+        /* async; the answer comes in usbredir_server_ctrl_status_complete() */
+        usbredir_server_submit_to_device(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)
+{
+    usbredir_server_do_control(priv, id, hdr, data, data_len,
+                               USBREDIR_SERVER_REPLY_CONTROL);
+}
+
+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 +896,84 @@ 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)
+{
+    struct usb_redir_control_packet_header resp = {
+        .endpoint = 0,
+        .status = usb_redir_cancelled,
+        .length = 0,
+    };
+    USBRedirServer *s = priv;
+    USBRedirServerPkt *rp;
+
+    /*
+     * The host has put this id in its cancelled queue and waits for one
+     * answer carrying it. The device may have answered already, so the
+     * request may no longer be on our list. Answer in both cases: the
+     * host reuses ids, and a leftover entry would eat a later answer.
+     */
+    QTAILQ_FOREACH(rp, &s->inflight, next) {
+        if (rp->redir_id == id) {
+            trace_usbredir_server_cancel(id, true);
+            usbredir_server_send_cancelled(s, rp);
+            usbredir_server_drop_pkt(s, rp);
+            return;
+        }
+    }
+
+    /*
+     * Already finished, so we no longer know what kind of transfer it
+     * was. The host retires the entry on the id alone, and its control
+     * handler ignores the endpoint field, so a control packet always
+     * works.
+     */
+    trace_usbredir_server_cancel(id, false);
+    usbredirparser_send_control_packet(s->parser, id, &resp, NULL, 0);
+    usbredirparser_do_write(s->parser);
+}
+
+/*
+ * 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_SETUP:
+    case USBREDIR_SERVER_CTRL_STATUS:
+        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;
+
+    /*
+     * 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_drop_pkt(s, rp);
+    }
+}
+
 /*
  * Parser setup and teardown
  */
@@ -360,13 +996,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 +1040,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;
@@ -486,6 +1130,9 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
         return;
     }
 
+    QTAILQ_INIT(&s->inflight);
+    memset(s->ep_type, USB_ENDPOINT_XFER_INVALID, sizeof(s->ep_type));
+
     /* One port: usbredir carries a single device. */
     usb_bus_new(&s->bus, sizeof(s->bus), &usbredir_server_bus_ops, dev);
     s->bus.no_auto_hub = true;
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index 2cc6a244b9..25219a018a 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -406,3 +406,8 @@ 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_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_ctrl_setup_complete(uint64_t id, int status, int actual) "id %" PRIu64 " status %d actual %d"
+usbredir_server_ctrl_status_complete(uint64_t id, int status) "id %" PRIu64 " status %d"
-- 
2.53.0


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

* [PATCH v3 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (4 preceding siblings ...)
  2026-10-12  2:16 ` [PATCH v3 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12 10:01   ` marcandre.lureau
  2026-10-12  2:16 ` [PATCH v3 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
                   ` (2 subsequent siblings)
  8 siblings, 1 reply; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, 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 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.

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

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 22083394ab..a46d7aec43 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -82,8 +82,16 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
 #define USBREDIR_SERVER_MAX_EP 32
 #define USBREDIR_SERVER_EP_IN_BASE 16
 
+/*
+ * 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_SETUP 0
 #define USBREDIR_SERVER_CTRL_STATUS 1
+#define USBREDIR_SERVER_BULK 2
+#define USBREDIR_SERVER_INTR 3
 
 /* Which message answers the host when a control transfer ends. */
 typedef enum {
@@ -96,7 +104,9 @@ typedef struct USBRedirServerPkt {
     USBPacket pkt;
 
     /* Saved headers 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.
@@ -428,6 +438,52 @@ static void usbredir_server_ctrl_status_complete(USBRedirServer *s,
     }
 }
 
+static void usbredir_server_bulk_complete(USBRedirServer *s,
+                                          USBRedirServerPkt *rp)
+{
+    struct usb_redir_bulk_packet_header resp = rp->bulk_hdr;
+    bool is_in = !!(rp->bulk_hdr.endpoint & USB_DIR_IN);
+    int actual = rp->pkt.actual_length;
+    int len;
+
+    resp.status = usbredir_server_status(rp->pkt.status);
+
+    /* Only an IN transfer carries data back. */
+    len = is_in ? actual : 0;
+    resp.length = len;
+    resp.length_high = len >> 16;
+
+    trace_usbredir_server_bulk_complete(rp->redir_id, resp.endpoint,
+                                        resp.status, actual);
+
+    usbredirparser_send_bulk_packet(s->parser, rp->redir_id, &resp,
+                                    len > 0 ? rp->data : NULL, len);
+    usbredirparser_do_write(s->parser);
+}
+
+static void usbredir_server_intr_complete(USBRedirServer *s,
+                                          USBRedirServerPkt *rp)
+{
+    struct usb_redir_interrupt_packet_header resp = rp->intr_hdr;
+    bool is_in = !!(rp->intr_hdr.endpoint & USB_DIR_IN);
+    int actual = rp->pkt.actual_length;
+    int len;
+
+    resp.status = usbredir_server_status(rp->pkt.status);
+
+    /* Only an IN transfer carries data back. */
+    len = is_in ? actual : 0;
+    resp.length = len;
+
+    trace_usbredir_server_intr_complete(rp->redir_id, resp.endpoint,
+                                        resp.status, actual);
+
+    usbredirparser_send_interrupt_packet(s->parser, rp->redir_id, &resp,
+                                         len > 0 ? rp->data : NULL,
+                                         len);
+    usbredirparser_do_write(s->parser);
+}
+
 /*
  * USB port ops
  */
@@ -497,6 +553,12 @@ static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
     case USBREDIR_SERVER_CTRL_STATUS:
         usbredir_server_ctrl_status_complete(s, rp);
         break;
+    case USBREDIR_SERVER_BULK:
+        usbredir_server_bulk_complete(s, rp);
+        break;
+    case USBREDIR_SERVER_INTR:
+        usbredir_server_intr_complete(s, rp);
+        break;
     }
 
     usbredir_server_pkt_free(rp);
@@ -873,6 +935,105 @@ 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;
+    USBDevice *device = usbredir_server_device(s);
+    bool is_in = !!(hdr->endpoint & USB_DIR_IN);
+    int pid = is_in ? USB_TOKEN_IN : USB_TOKEN_OUT;
+    struct usb_redir_bulk_packet_header resp;
+    int ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServerPkt *rp;
+    USBEndpoint *ep;
+    uint32_t len;
+
+    if (!s->host_connected || !device || !device->attached) {
+        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) {
+        resp = *hdr;
+        resp.status = usb_redir_inval;
+        resp.length = 0;
+        resp.length_high = 0;
+
+        trace_usbredir_server_bulk_too_big(id, hdr->endpoint, len);
+        usbredirparser_send_bulk_packet(s->parser, id, &resp, NULL, 0);
+        usbredirparser_do_write(s->parser);
+        return;
+    }
+
+    ep = usb_ep_get(device, pid, ep_nr);
+    rp = usbredir_server_pkt_alloc(len);
+    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);
+    usbredir_server_submit_to_device(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;
+    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;
+    int len;
+
+    if (!s->host_connected || !device || !device->attached) {
+        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);
+    rp->redir_id = id;
+    rp->type = USBREDIR_SERVER_INTR;
+    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);
+    usbredir_server_submit_to_device(s, rp);
+}
+
 static void usbredir_server_filter_reject(void *priv)
 {
     trace_usbredir_server_filter_reject();
@@ -896,6 +1057,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)
 {
     struct usb_redir_control_packet_header resp = {
@@ -944,7 +1123,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_SETUP:
@@ -955,6 +1136,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 = 0;
+        bulk.length_high = 0;
+        usbredirparser_send_bulk_packet(s->parser, rp->redir_id,
+                                        &bulk, NULL, 0);
+        break;
+    case USBREDIR_SERVER_INTR:
+        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;
     }
@@ -997,6 +1193,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. */
@@ -1008,7 +1206,12 @@ 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 */
     usbredirparser_caps_set_cap(caps, usb_redir_cap_connect_device_version);
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index 25219a018a..c9ab80c6ba 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -411,3 +411,8 @@ usbredir_server_ep_info(unsigned idx, uint8_t type, uint16_t mps, uint8_t interv
 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_ctrl_setup_complete(uint64_t id, int status, int actual) "id %" PRIu64 " status %d actual %d"
 usbredir_server_ctrl_status_complete(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_interrupt(uint64_t id, uint8_t ep, size_t len) "id %" PRIu64 " ep 0x%02x len %zu"
+usbredir_server_intr_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] 17+ messages in thread

* [PATCH v3 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (5 preceding siblings ...)
  2026-10-12  2:16 ` [PATCH v3 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12 10:01   ` marcandre.lureau
  2026-10-12  2:16 ` [PATCH v3 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
  2026-10-12  2:19 ` [PATCH v3 0/8] hw/usb: Add a " Jamin Lin
  8 siblings, 1 reply; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, 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.

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

diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index a46d7aec43..86365bc168 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"
@@ -82,16 +83,30 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
 #define USBREDIR_SERVER_MAX_EP 32
 #define USBREDIR_SERVER_EP_IN_BASE 16
 
+/* An endpoint number is 4 bits, so 0 to 15. */
+#define USBREDIR_SERVER_MAX_EP_NR 16
+
 /*
  * 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)
 
+/* Buffer size for an interrupt IN endpoint before its descriptor is seen. */
+#define USBREDIR_SERVER_INTR_DEFAULT_LEN 64
+
+/*
+ * Ask again this often after an endpoint answered NAK. Most devices wake
+ * the bus when data arrives, and then the next ask happens at once. This
+ * timer is for the devices that do not wake the bus.
+ */
+#define USBREDIR_SERVER_INTR_RETRY_MS 1000
+
 #define USBREDIR_SERVER_CTRL_SETUP 0
 #define USBREDIR_SERVER_CTRL_STATUS 1
 #define USBREDIR_SERVER_BULK 2
 #define USBREDIR_SERVER_INTR 3
+#define USBREDIR_SERVER_INTR_STREAM 4
 
 /* Which message answers the host when a control transfer ends. */
 typedef enum {
@@ -140,6 +155,14 @@ struct USBRedirServer {
     bool host_connected;
     bool device_announced;
 
+    /*
+     * Interrupt IN streaming, indexed by endpoint number. intr_bh asks the
+     * device again; intr_retry does the same after a delay on NAK.
+     */
+    bool intr_in_started[USBREDIR_SERVER_MAX_EP_NR];
+    QEMUBH *intr_bh;
+    QEMUTimer *intr_retry;
+
     /* In-flight packet tracking */
     QTAILQ_HEAD(, USBRedirServerPkt) inflight;
     uint64_t next_id;
@@ -484,6 +507,63 @@ static void usbredir_server_intr_complete(USBRedirServer *s,
     usbredirparser_do_write(s->parser);
 }
 
+/*
+ * The device answered a parked 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. Let the BH ask, or the retry timer after a NAK.
+ */
+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 ep_nr = resp.endpoint & 0x0f;
+    USBPacket *p = &rp->pkt;
+    int actual = p->actual_length;
+    bool retry = false;
+
+    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:
+        /* nothing to report yet; ask again shortly */
+        retry = true;
+        break;
+    default:
+        /*
+         * The endpoint stalled or failed. There is no data to send, so
+         * send a status message. The host keeps that status and gives it
+         * to its guest. On a stall the host also ends the stream, so end
+         * it here too. A later request from the guest starts a new one.
+         */
+        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);
+        s->intr_in_started[ep_nr] = false;
+        break;
+    }
+
+    if (s->intr_in_started[ep_nr]) {
+        if (retry) {
+            timer_mod(s->intr_retry,
+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
+                      (int64_t)USBREDIR_SERVER_INTR_RETRY_MS * SCALE_MS);
+        } else {
+            qemu_bh_schedule(s->intr_bh);
+        }
+    }
+}
+
 /*
  * USB port ops
  */
@@ -559,6 +639,9 @@ static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
     case USBREDIR_SERVER_INTR:
         usbredir_server_intr_complete(s, rp);
         break;
+    case USBREDIR_SERVER_INTR_STREAM:
+        usbredir_server_intr_stream_complete(s, rp);
+        break;
     }
 
     usbredir_server_pkt_free(rp);
@@ -576,7 +659,25 @@ static USBPortOps usbredir_server_port_ops = {
  * USB bus ops
  */
 
+static void usbredir_server_wakeup_ep(USBBus *bus, USBEndpoint *ep,
+                                      unsigned int stream)
+{
+    USBRedirServer *s = container_of(bus, USBRedirServer, bus);
+    int ep_nr = ep->nr;
+
+    /*
+     * The device has data. A streaming interrupt IN endpoint may have
+     * answered NAK. Ask now instead of waiting for the retry timer.
+     * Every other transfer already has its request on the device.
+     */
+    if (ep->pid == USB_TOKEN_IN && ep_nr < USBREDIR_SERVER_MAX_EP_NR &&
+        s->intr_in_started[ep_nr]) {
+        qemu_bh_schedule(s->intr_bh);
+    }
+}
+
 static USBBusOps usbredir_server_bus_ops = {
+    .wakeup_endpoint = usbredir_server_wakeup_ep,
 };
 
 /*
@@ -627,6 +728,91 @@ static void usbredir_server_drop_pkt(USBRedirServer *s,
     usbredir_server_pkt_free(rp);
 }
 
+/*
+ * Interrupt IN streaming
+ */
+
+static bool usbredir_server_intr_inflight(USBRedirServer *s, int ep_nr)
+{
+    USBRedirServerPkt *rp;
+
+    QTAILQ_FOREACH(rp, &s->inflight, next) {
+        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_park(USBRedirServer *s, int ep_nr)
+{
+    USBDevice *device = usbredir_server_device(s);
+    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->intr_in_started[ep_nr] ||
+        usbredir_server_intr_inflight(s, ep_nr)) {
+        return;
+    }
+
+    len = s->ep_max_packet[ep_nr + USBREDIR_SERVER_EP_IN_BASE];
+    if (len == 0) {
+        len = USBREDIR_SERVER_INTR_DEFAULT_LEN;
+    }
+    ep = usb_ep_get(device, USB_TOKEN_IN, ep_nr);
+    rp = usbredir_server_pkt_alloc(len);
+    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);
+
+    trace_usbredir_server_intr_park(ep_nr, len);
+    usbredir_server_submit_to_device(s, rp);
+}
+
+/* BH and timer callback: ask again on every streaming endpoint. */
+static void usbredir_server_intr_kick(void *opaque)
+{
+    USBRedirServer *s = opaque;
+    int i;
+
+    /* Endpoint 0 is the control endpoint. It never streams. */
+    for (i = 1; i < USBREDIR_SERVER_MAX_EP_NR; i++) {
+        usbredir_server_intr_park(s, i);
+    }
+}
+
+static void usbredir_server_intr_cancel(USBRedirServer *s, int ep_nr)
+{
+    USBRedirServerPkt *tmp;
+    USBRedirServerPkt *rp;
+
+    QTAILQ_FOREACH_SAFE(rp, &s->inflight, next, tmp) {
+        if (rp->type != USBREDIR_SERVER_INTR_STREAM ||
+            (rp->intr_hdr.endpoint & 0x0f) != ep_nr) {
+            continue;
+        }
+        usbredir_server_drop_pkt(s, rp);
+    }
+}
+
 /*
  * usbredirparser I/O and logging callbacks
  */
@@ -1057,6 +1243,58 @@ 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;
+    USBDevice *device = usbredir_server_device(s);
+    int ep_nr = hdr->endpoint & 0x0f;
+
+    /*
+     * Endpoint 0 is control and an OUT endpoint never streams. There also
+     * has to be a host to send to and a device to ask.
+     */
+    if (ep_nr == 0 || !(hdr->endpoint & USB_DIR_IN) ||
+        !s->host_connected || !device || !device->attached) {
+        st.status = usb_redir_ioerror;
+    } else {
+        s->intr_in_started[ep_nr] = true;
+    }
+
+    trace_usbredir_server_intr_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_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 ep_nr = hdr->endpoint & 0x0f;
+    USBRedirServer *s = priv;
+
+    /* Endpoint 0 is control, and an OUT endpoint never streams. */
+    if (ep_nr == 0 || !(hdr->endpoint & USB_DIR_IN)) {
+        return;
+    }
+
+    trace_usbredir_server_intr_stop(hdr->endpoint);
+    s->intr_in_started[ep_nr] = false;
+    usbredir_server_intr_cancel(s, ep_nr);
+}
+
 static void usbredir_server_alloc_bulk_streams(void *priv, uint64_t id,
     struct usb_redir_alloc_bulk_streams_header *hdr)
 {
@@ -1161,6 +1399,14 @@ static void usbredir_server_stop_transfers(USBRedirServer *s)
 {
     USBRedirServerPkt *rp;
 
+    memset(s->intr_in_started, 0, sizeof(s->intr_in_started));
+    if (s->intr_bh) {
+        qemu_bh_cancel(s->intr_bh);
+    }
+    if (s->intr_retry) {
+        timer_del(s->intr_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.
@@ -1206,6 +1452,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 =
@@ -1343,6 +1593,10 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
                       USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL |
                       USB_SPEED_MASK_HIGH);
 
+    s->intr_retry = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+                                 usbredir_server_intr_kick, s);
+    s->intr_bh = qemu_bh_new_guarded(usbredir_server_intr_kick, s,
+                                     &dev->mem_reentrancy_guard);
     s->chardev_close_bh = qemu_bh_new_guarded(usbredir_server_chardev_close_bh,
                                               s, &dev->mem_reentrancy_guard);
 
@@ -1360,6 +1614,16 @@ static void usbredir_server_unrealize(DeviceState *dev)
     qemu_chr_fe_deinit(&s->cs, true);
     usbredir_server_destroy_parser(s);
 
+    if (s->intr_retry) {
+        timer_free(s->intr_retry);
+        s->intr_retry = NULL;
+    }
+
+    if (s->intr_bh) {
+        qemu_bh_delete(s->intr_bh);
+        s->intr_bh = NULL;
+    }
+
     if (s->chardev_close_bh) {
         qemu_bh_delete(s->chardev_close_bh);
         s->chardev_close_bh = NULL;
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index c9ab80c6ba..931996e8cd 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -416,3 +416,7 @@ usbredir_server_bulk_complete(uint64_t id, uint8_t ep, int status, int actual) "
 usbredir_server_bulk_too_big(uint64_t id, uint8_t ep, uint32_t len) "id %" PRIu64 " ep 0x%02x len %u"
 usbredir_server_interrupt(uint64_t id, uint8_t ep, size_t len) "id %" PRIu64 " ep 0x%02x len %zu"
 usbredir_server_intr_complete(uint64_t id, uint8_t ep, int status, int actual) "id %" PRIu64 " ep 0x%02x status %d actual %d"
+usbredir_server_intr_start(uint8_t ep, int status) "start_interrupt_receiving ep 0x%02x -> status %d"
+usbredir_server_intr_stop(uint8_t ep) "stop_interrupt_receiving ep 0x%02x"
+usbredir_server_intr_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] 17+ messages in thread

* [PATCH v3 8/8] hw/arm/aspeed: Enable the usbredir server transport
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (6 preceding siblings ...)
  2026-10-12  2:16 ` [PATCH v3 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
@ 2026-10-12  2:16 ` Jamin Lin
  2026-10-12 10:01   ` marcandre.lureau
  2026-10-12  2:19 ` [PATCH v3 0/8] hw/usb: Add a " Jamin Lin
  8 siblings, 1 reply; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:16 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, 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>
---
 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] 17+ messages in thread

* RE: [PATCH v3 0/8] hw/usb: Add a usbredir server transport
  2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
                   ` (7 preceding siblings ...)
  2026-10-12  2:16 ` [PATCH v3 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
@ 2026-10-12  2:19 ` Jamin Lin
  8 siblings, 0 replies; 17+ messages in thread
From: Jamin Lin @ 2026-10-12  2:19 UTC (permalink / raw)
  To: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, marcandre.lureau@redhat.com
  Cc: Troy Lee

Sorry lost to add Marc-André Lureau <marcandre.lureau@redhat.com>

> -----Original Message-----
> From: Jamin Lin
> Sent: Monday, October 12, 2026 10:17 AM
> To: Cédric Le Goater <clg@kaod.org>; Peter Maydell
> <peter.maydell@linaro.org>; Steven Lee <steven_lee@aspeedtech.com>; Troy
> Lee <leetroy@gmail.com>; Kane Chen <kane_chen@aspeedtech.com>;
> Andrew Jeffery <andrew@codeconstruct.com.au>; Joel Stanley
> <joel@jms.id.au>; open list:ASPEED BMCs <qemu-arm@nongnu.org>; open
> list:All patches CC here <qemu-devel@nongnu.org>
> Cc: Jamin Lin <jamin_lin@aspeedtech.com>; Troy Lee
> <troy_lee@aspeedtech.com>
> Subject: [PATCH v3 0/8] hw/usb: Add a usbredir server transport
> 
> This series adds a usbredir server transport to redirect a USB device emulated
> in one QEMU instance to a USB host controller emulated in another one over
> the usbredir protocol.
> 
> The v1 series included both the usbredir server transport and the ASPEED
> AST1030 USB Device Controller (UDC) support. Following review feedback, this
> series contains only the usbredir server transport. The ASPEED
> AST1030 UDC support will be sent separately.
> 
> This work is part of a larger plan to model USB device-side support on ASPEED
> BMC/BIC SoCs, which has three goals:
> 
> 1. Model the ASPEED UDC (AST2600 / AST1030). The AST2600 also has USB
> host
>    (EHCI) controllers, so that work targets the AST2600 UDC: its gadget can
>    be attached to the SoC's own EHCI bus, letting the guest enumerate its
>    own gadget and exercise the UDC end-to-end.  [Done]
> 
> https://lore.kernel.org/qemu-devel/20260902021542.3194812-1-jamin_lin@as
> peedtech.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.
> 
> 
> Jamin Lin (8):
>   hw/usb/bus: Let a bus opt out of automatic hub insertion
>   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             |    3 +-
>  hw/usb/redirect-server.c | 1669
> ++++++++++++++++++++++++++++++++++++++
>  hw/usb/meson.build       |    3 +-
>  hw/usb/trace-events      |   27 +
>  6 files changed, 1708 insertions(+), 2 deletions(-)  create mode 100644
> hw/usb/redirect-server.c
> 
> --
> 2.53.0


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

* Re: [PATCH v3 2/8] hw/usb/redirect-server: Add a usbredir server device
  2026-10-12  2:16 ` [PATCH v3 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
@ 2026-10-12 10:01   ` marcandre.lureau
  0 siblings, 0 replies; 17+ messages in thread
From: marcandre.lureau @ 2026-10-12 10:01 UTC (permalink / raw)
  To: Jamin Lin
  Cc: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, Troy Lee

On Mon, 12 Oct 2026 02:16:37 +0000, Jamin Lin <jamin_lin@aspeedtech.com> wrote:
> 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.
> 
> [...]

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

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



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

* Re: [PATCH v3 4/8] hw/usb/redirect-server: Announce the exported device
  2026-10-12  2:16 ` [PATCH v3 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
@ 2026-10-12 10:01   ` marcandre.lureau
  0 siblings, 0 replies; 17+ messages in thread
From: marcandre.lureau @ 2026-10-12 10:01 UTC (permalink / raw)
  To: Jamin Lin
  Cc: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, Troy Lee

On Mon, 12 Oct 2026 02:16:40 +0000, Jamin Lin <jamin_lin@aspeedtech.com> wrote:
> 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.
> 
> [...]

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

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



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

* Re: [PATCH v3 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers
  2026-10-12  2:16 ` [PATCH v3 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
@ 2026-10-12 10:01   ` marcandre.lureau
  0 siblings, 0 replies; 17+ messages in thread
From: marcandre.lureau @ 2026-10-12 10:01 UTC (permalink / raw)
  To: Jamin Lin
  Cc: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, Troy Lee

On Mon, 12 Oct 2026 02:16:42 +0000, Jamin Lin <jamin_lin@aspeedtech.com> wrote:
> 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 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.
> 
> [...]

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

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



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

* Re: [PATCH v3 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints
  2026-10-12  2:16 ` [PATCH v3 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
@ 2026-10-12 10:01   ` marcandre.lureau
  0 siblings, 0 replies; 17+ messages in thread
From: marcandre.lureau @ 2026-10-12 10:01 UTC (permalink / raw)
  To: Jamin Lin
  Cc: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, Troy Lee

On Mon, 12 Oct 2026 02:16:43 +0000, Jamin Lin <jamin_lin@aspeedtech.com> wrote:
> 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.
> 
> [...]

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

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



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

* Re: [PATCH v3 5/8] hw/usb/redirect-server: Implement control transfers
  2026-10-12  2:16 ` [PATCH v3 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
@ 2026-10-12 10:01   ` marcandre.lureau
  2026-10-13  1:30     ` Jamin Lin
  0 siblings, 1 reply; 17+ messages in thread
From: marcandre.lureau @ 2026-10-12 10:01 UTC (permalink / raw)
  To: Jamin Lin
  Cc: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, Troy Lee

> The host sends a control_packet. Turn it into the SETUP, DATA and
> STATUS tokens the QEMU USB core wants, run them on the device, and
> send the answer back.
> 
> 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.
> 
> 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:
> bulk, interrupt or control. 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.
> 
> Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> Message-ID: <20261012021634.2006424-6-jamin_lin@aspeedtech.com>
>
> diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
> index 05ee0f38736e..22083394ab41 100644
> --- a/hw/usb/redirect-server.c
> +++ b/hw/usb/redirect-server.c
> @@ -63,9 +63,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 +75,41 @@ 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
> +
> +#define USBREDIR_SERVER_CTRL_SETUP 0
> +#define USBREDIR_SERVER_CTRL_STATUS 1
> +
> +/* 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;
> +
> +typedef struct USBRedirServerPkt {
> +    USBPacket pkt;
> +
> +    /* Saved headers 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; data_size is how big it is.
> +     */
> +    uint8_t *data;
> +    int data_size;
> +    QTAILQ_ENTRY(USBRedirServerPkt) next;
> +    USBRedirServerReply reply;
> +    uint64_t redir_id;
> +    int type;
> +} USBRedirServerPkt;
> +
>  struct USBRedirServer {
>      SysBusDevice parent_obj;
>  
> @@ -92,8 +129,23 @@ struct USBRedirServer {
>      guint watch;
>      bool host_connected;
>      bool device_announced;
> +
> +    /* In-flight packet tracking */
> +    QTAILQ_HEAD(, USBRedirServerPkt) inflight;
> +    uint64_t next_id;
> +
> +    /* Endpoint tables for the usbredir ep_info message. */
> +    uint8_t ep_type[USBREDIR_SERVER_MAX_EP];
> +    uint16_t ep_max_packet[USBREDIR_SERVER_MAX_EP];
> +    uint8_t ep_interval[USBREDIR_SERVER_MAX_EP];
> +    uint8_t ep_iface[USBREDIR_SERVER_MAX_EP];
>  };
>  
> +static void usbredir_server_pkt_free(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 +156,98 @@ static USBDevice *usbredir_server_device(USBRedirServer *s)
>  }
>  
>  /*
> - * Device announcement
> + * Descriptor snooping and device announcement
>   */
>  
> +static void usbredir_server_record_endpoint(USBRedirServer *s,
> +                                            const USBDescriptor *desc,
> +                                            uint8_t iface)
> +{
> +    uint8_t addr;
> +    int ep_nr;
> +    int idx;
> +
> +    if (desc->bLength < 7) {
> +        return;
> +    }
> +
> +    addr = desc->u.endpoint.bEndpointAddress;
> +    ep_nr = addr & 0x0f;
> +    idx = (addr & USB_DIR_IN) ? ep_nr + USBREDIR_SERVER_EP_IN_BASE : ep_nr;
> +    if (ep_nr < 1 || idx >= USBREDIR_SERVER_MAX_EP) {
> +        return;
> +    }
> +
> +    s->ep_type[idx] = desc->u.endpoint.bmAttributes & 0x03;
> +    s->ep_max_packet[idx] = (desc->u.endpoint.wMaxPacketSize_hi << 8) |
> +                            desc->u.endpoint.wMaxPacketSize_lo;
> +    s->ep_interval[idx] = desc->u.endpoint.bInterval;
> +    s->ep_iface[idx] = iface;
> +}
> +
> +/*
> + * A device that only passes transfers through never gets its endpoint
> + * types filled in: they stay INVALID and there is nothing to put in
> + * ep_info. Take them from the configuration descriptor as it goes past.
> + * Without them the host sees type 255 and refuses to move data.
> + */
> +static void usbredir_server_snoop_config_desc(USBRedirServer *s,
> +                                              const uint8_t *data, int len)
> +{
> +    const USBDescriptor *desc;
> +    uint8_t iface = 0;
> +    int i = 0;
> +
> +    while (i + 2 <= len) {
> +        desc = (const USBDescriptor *)(data + i);
> +
> +        if (desc->bLength < 2 || i + desc->bLength > len) {
> +            break;
> +        }
> +
> +        if (desc->bDescriptorType == USB_DT_INTERFACE && desc->bLength >= 3) {
> +            iface = desc->u.interface.bInterfaceNumber;

This records the interface number but not bAlternateSetting. When the
config descriptor carries two alternates for the same interface, both
are walked and their endpoints overwrite each other in the ep tables.
After SET_INTERFACE selects alternate 0, ep_info may still describe the
endpoints of alternate 1.

You should be able to cache the config descriptor and re-parses it when the
active alternate changes at ctrl_status_complete(), when sending the new
send_ep_info().

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



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

* Re: [PATCH v3 8/8] hw/arm/aspeed: Enable the usbredir server transport
  2026-10-12  2:16 ` [PATCH v3 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
@ 2026-10-12 10:01   ` marcandre.lureau
  0 siblings, 0 replies; 17+ messages in thread
From: marcandre.lureau @ 2026-10-12 10:01 UTC (permalink / raw)
  To: Jamin Lin
  Cc: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, Troy Lee

On Mon, 12 Oct 2026 02:16:45 +0000, Jamin Lin <jamin_lin@aspeedtech.com> wrote:
> 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.
> 
> [...]

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

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



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

* RE: [PATCH v3 5/8] hw/usb/redirect-server: Implement control transfers
  2026-10-12 10:01   ` marcandre.lureau
@ 2026-10-13  1:30     ` Jamin Lin
  0 siblings, 0 replies; 17+ messages in thread
From: Jamin Lin @ 2026-10-13  1:30 UTC (permalink / raw)
  To: marcandre.lureau@redhat.com
  Cc: Cédric Le Goater, Peter Maydell, Steven Lee, Troy Lee,
	Kane Chen, Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
	open list:All patches CC here, Troy Lee

Hi Marc-André Lureau

> Subject: Re: [PATCH v3 5/8] hw/usb/redirect-server: Implement control
> transfers
> 
> > The host sends a control_packet. Turn it into the SETUP, DATA and
> > STATUS tokens the QEMU USB core wants, run them on the device, and
> > send the answer back.
> >
> > 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.
> >
> > 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:
> > bulk, interrupt or control. 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.
> >
> > Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> > Message-ID: <20261012021634.2006424-6-jamin_lin@aspeedtech.com>
> >
> > diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c index
> > 05ee0f38736e..22083394ab41 100644
> > --- a/hw/usb/redirect-server.c
> > +++ b/hw/usb/redirect-server.c
> > @@ -63,9 +63,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 +75,41 @@ 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
> > +
> > +#define USBREDIR_SERVER_CTRL_SETUP 0
> > +#define USBREDIR_SERVER_CTRL_STATUS 1
> > +
> > +/* 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;
> > +
> > +typedef struct USBRedirServerPkt {
> > +    USBPacket pkt;
> > +
> > +    /* Saved headers 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; data_size is how big it is.
> > +     */
> > +    uint8_t *data;
> > +    int data_size;
> > +    QTAILQ_ENTRY(USBRedirServerPkt) next;
> > +    USBRedirServerReply reply;
> > +    uint64_t redir_id;
> > +    int type;
> > +} USBRedirServerPkt;
> > +
> >  struct USBRedirServer {
> >      SysBusDevice parent_obj;
> >
> > @@ -92,8 +129,23 @@ struct USBRedirServer {
> >      guint watch;
> >      bool host_connected;
> >      bool device_announced;
> > +
> > +    /* In-flight packet tracking */
> > +    QTAILQ_HEAD(, USBRedirServerPkt) inflight;
> > +    uint64_t next_id;
> > +
> > +    /* Endpoint tables for the usbredir ep_info message. */
> > +    uint8_t ep_type[USBREDIR_SERVER_MAX_EP];
> > +    uint16_t ep_max_packet[USBREDIR_SERVER_MAX_EP];
> > +    uint8_t ep_interval[USBREDIR_SERVER_MAX_EP];
> > +    uint8_t ep_iface[USBREDIR_SERVER_MAX_EP];
> >  };
> >
> > +static void usbredir_server_pkt_free(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 +156,98 @@ static USBDevice
> > *usbredir_server_device(USBRedirServer *s)  }
> >
> >  /*
> > - * Device announcement
> > + * Descriptor snooping and device announcement
> >   */
> >
> > +static void usbredir_server_record_endpoint(USBRedirServer *s,
> > +                                            const USBDescriptor
> *desc,
> > +                                            uint8_t iface) {
> > +    uint8_t addr;
> > +    int ep_nr;
> > +    int idx;
> > +
> > +    if (desc->bLength < 7) {
> > +        return;
> > +    }
> > +
> > +    addr = desc->u.endpoint.bEndpointAddress;
> > +    ep_nr = addr & 0x0f;
> > +    idx = (addr & USB_DIR_IN) ? ep_nr + USBREDIR_SERVER_EP_IN_BASE :
> ep_nr;
> > +    if (ep_nr < 1 || idx >= USBREDIR_SERVER_MAX_EP) {
> > +        return;
> > +    }
> > +
> > +    s->ep_type[idx] = desc->u.endpoint.bmAttributes & 0x03;
> > +    s->ep_max_packet[idx] = (desc->u.endpoint.wMaxPacketSize_hi << 8)
> |
> > +                            desc->u.endpoint.wMaxPacketSize_lo;
> > +    s->ep_interval[idx] = desc->u.endpoint.bInterval;
> > +    s->ep_iface[idx] = iface;
> > +}
> > +
> > +/*
> > + * A device that only passes transfers through never gets its
> > +endpoint
> > + * types filled in: they stay INVALID and there is nothing to put in
> > + * ep_info. Take them from the configuration descriptor as it goes past.
> > + * Without them the host sees type 255 and refuses to move data.
> > + */
> > +static void usbredir_server_snoop_config_desc(USBRedirServer *s,
> > +                                              const uint8_t
> *data,
> > +int len) {
> > +    const USBDescriptor *desc;
> > +    uint8_t iface = 0;
> > +    int i = 0;
> > +
> > +    while (i + 2 <= len) {
> > +        desc = (const USBDescriptor *)(data + i);
> > +
> > +        if (desc->bLength < 2 || i + desc->bLength > len) {
> > +            break;
> > +        }
> > +
> > +        if (desc->bDescriptorType == USB_DT_INTERFACE &&
> desc->bLength >= 3) {
> > +            iface = desc->u.interface.bInterfaceNumber;
> 
> This records the interface number but not bAlternateSetting. When the config
> descriptor carries two alternates for the same interface, both are walked and
> their endpoints overwrite each other in the ep tables.
> After SET_INTERFACE selects alternate 0, ep_info may still describe the
> endpoints of alternate 1.
> 
> You should be able to cache the config descriptor and re-parses it when the
> active alternate changes at ctrl_status_complete(), when sending the new
> send_ep_info().
> 

Thanks for the review and suggestion.
Will fix it.

Jamin

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


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

end of thread, other threads:[~2026-10-13  1:31 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-12  2:16 [PATCH v3 0/8] hw/usb: Add a usbredir server transport Jamin Lin
2026-10-12  2:16 ` [PATCH v3 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
2026-10-12  2:16 ` [PATCH v3 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
2026-10-12 10:01   ` marcandre.lureau
2026-10-12  2:16 ` [PATCH v3 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
2026-10-12  2:16 ` [PATCH v3 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
2026-10-12 10:01   ` marcandre.lureau
2026-10-12  2:16 ` [PATCH v3 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
2026-10-12 10:01   ` marcandre.lureau
2026-10-13  1:30     ` Jamin Lin
2026-10-12  2:16 ` [PATCH v3 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
2026-10-12 10:01   ` marcandre.lureau
2026-10-12  2:16 ` [PATCH v3 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
2026-10-12 10:01   ` marcandre.lureau
2026-10-12  2:16 ` [PATCH v3 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
2026-10-12 10:01   ` marcandre.lureau
2026-10-12  2:19 ` [PATCH v3 0/8] hw/usb: Add a " 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.