* [PATCH v2 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-05 3:20 ` [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
` (7 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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
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>
---
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.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion
2026-10-05 3:20 ` [PATCH v2 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
@ 2026-10-05 8:59 ` marcandre.lureau
0 siblings, 0 replies; 19+ messages in thread
From: marcandre.lureau @ 2026-10-05 8:59 UTC (permalink / raw)
To: Jamin Lin
Cc: clg, 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, 05 Oct 2026 03:20:24 +0000, Jamin Lin <jamin_lin@aspeedtech.com> wrote:
> 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.
>
> [...]
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
--
Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
2026-10-05 3:20 ` [PATCH v2 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-05 3:20 ` [PATCH v2 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
` (6 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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 "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 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
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>
---
include/hw/usb/redirect-server.h | 27 +++++++
hw/usb/redirect-server.c | 130 +++++++++++++++++++++++++++++++
hw/usb/meson.build | 3 +-
3 files changed, 159 insertions(+), 1 deletion(-)
create mode 100644 include/hw/usb/redirect-server.h
create mode 100644 hw/usb/redirect-server.c
diff --git a/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
new file mode 100644
index 0000000000..e81d4e0608
--- /dev/null
+++ b/include/hw/usb/redirect-server.h
@@ -0,0 +1,27 @@
+/*
+ * USB redirector, server side
+ *
+ * Copyright (c) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_USB_REDIRECT_SERVER_H
+#define HW_USB_REDIRECT_SERVER_H
+
+#include "hw/core/sysbus.h"
+#include "hw/usb/usb.h"
+#include "qom/object.h"
+
+#define TYPE_USB_REDIR_SERVER "usb-redir-server"
+OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
+
+struct USBRedirServer {
+ SysBusDevice parent_obj;
+
+ /* USB bus */
+ USBBus bus;
+ USBPort port;
+};
+
+#endif /* HW_USB_REDIRECT_SERVER_H */
diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
new file mode 100644
index 0000000000..5f365f0a35
--- /dev/null
+++ b/hw/usb/redirect-server.c
@@ -0,0 +1,130 @@
+/*
+ * 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 |
+ * 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 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/usb/redirect-server.h"
+
+/*
+ * 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.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device
2026-10-05 3:20 ` [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
@ 2026-10-05 8:59 ` marcandre.lureau
2026-10-07 2:15 ` Jamin Lin
0 siblings, 1 reply; 19+ messages in thread
From: marcandre.lureau @ 2026-10-05 8:59 UTC (permalink / raw)
To: Jamin Lin
Cc: clg, Peter Maydell, Steven Lee, Troy Lee, Kane Chen,
Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
open list:All patches CC here, Troy Lee
> Add "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 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
> 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>
> Message-ID: <20261005032022.3980903-3-jamin_lin@aspeedtech.com>
>
> diff --git a/hw/usb/meson.build b/hw/usb/meson.build
> index 429dcd6a24f7..0061b935e6bb 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
>
> diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
> new file mode 100644
> index 000000000000..5f365f0a35fc
> --- /dev/null
> +++ b/hw/usb/redirect-server.c
> @@ -0,0 +1,130 @@
> +/*
> + * 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 |
> + * 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 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/usb/redirect-server.h"
> +
> +/*
> + * 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/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
What do you need a header for? If there is nothing to share with other units, better
keep it all in .c.
--
Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 19+ messages in thread* RE: [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device
2026-10-05 8:59 ` marcandre.lureau
@ 2026-10-07 2:15 ` Jamin Lin
2026-10-07 6:11 ` Marc-André Lureau
0 siblings, 1 reply; 19+ messages in thread
From: Jamin Lin @ 2026-10-07 2:15 UTC (permalink / raw)
To: marcandre.lureau@redhat.com
Cc: clg@kaod.org, 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
> > diff --git a/include/hw/usb/redirect-server.h
> > b/include/hw/usb/redirect-server.h
>
> What do you need a header for? If there is nothing to share with other units,
> better keep it all in .c.
>
> --
> Marc-André Lureau <marcandre.lureau@redhat.com>
Thanks for your suggestion and review.
Nothing else includes it. The main reason for having a separate header file is readability.
redirect-server.c is already about 1600 lines, so I thought keeping the state structure
and constants in a separate file would make the code easier to follow.
Would hw/usb/redirect-server.h be OK instead?
If you still prefer to keep everything in the .c file, I will move it there for v3.
Thanks,
Jamin
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device
2026-10-07 2:15 ` Jamin Lin
@ 2026-10-07 6:11 ` Marc-André Lureau
2026-10-07 8:43 ` Jamin Lin
0 siblings, 1 reply; 19+ messages in thread
From: Marc-André Lureau @ 2026-10-07 6:11 UTC (permalink / raw)
To: Jamin Lin
Cc: clg@kaod.org, 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
On Wed, Oct 7, 2026 at 6:16 AM Jamin Lin <jamin_lin@aspeedtech.com> wrote:
>
> Hi Marc-André Lureau
>
> > > diff --git a/include/hw/usb/redirect-server.h
> > > b/include/hw/usb/redirect-server.h
> >
> > What do you need a header for? If there is nothing to share with other units,
> > better keep it all in .c.
> >
> > --
> > Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Thanks for your suggestion and review.
>
> Nothing else includes it. The main reason for having a separate header file is readability.
> redirect-server.c is already about 1600 lines, so I thought keeping the state structure
> and constants in a separate file would make the code easier to follow.
>
> Would hw/usb/redirect-server.h be OK instead?
Yes - I don't think we have strict rules about headers
> If you still prefer to keep everything in the .c file, I will move it there for v3.
That would be "my" preference. I don't mind large files as long as
they don't mix various concerns/domains/levels.
^ permalink raw reply [flat|nested] 19+ messages in thread
* RE: [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device
2026-10-07 6:11 ` Marc-André Lureau
@ 2026-10-07 8:43 ` Jamin Lin
0 siblings, 0 replies; 19+ messages in thread
From: Jamin Lin @ 2026-10-07 8:43 UTC (permalink / raw)
To: Marc-André Lureau
Cc: clg@kaod.org, 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 v2 2/8] hw/usb/redirect-server: Add a usbredir server
> device
>
> Hi
>
> On Wed, Oct 7, 2026 at 6:16 AM Jamin Lin <jamin_lin@aspeedtech.com>
> wrote:
> >
> > Hi Marc-André Lureau
> >
> > > > diff --git a/include/hw/usb/redirect-server.h
> > > > b/include/hw/usb/redirect-server.h
> > >
> > > What do you need a header for? If there is nothing to share with
> > > other units, better keep it all in .c.
> > >
> > > --
> > > Marc-André Lureau <marcandre.lureau@redhat.com>
> >
> > Thanks for your suggestion and review.
> >
> > Nothing else includes it. The main reason for having a separate header file is
> readability.
> > redirect-server.c is already about 1600 lines, so I thought keeping
> > the state structure and constants in a separate file would make the code
> easier to follow.
> >
> > Would hw/usb/redirect-server.h be OK instead?
>
> Yes - I don't think we have strict rules about headers
>
> > If you still prefer to keep everything in the .c file, I will move it there for v3.
>
> That would be "my" preference. I don't mind large files as long as they don't
> mix various concerns/domains/levels.
Thanks for the review and suggestion.
Will do.
Jamin
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
2026-10-05 3:20 ` [PATCH v2 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
2026-10-05 3:20 ` [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-05 3:20 ` [PATCH v2 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
` (5 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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
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>
---
include/hw/usb/redirect-server.h | 15 ++
hw/usb/redirect-server.c | 268 +++++++++++++++++++++++++++++++
hw/usb/trace-events | 6 +
3 files changed, 289 insertions(+)
diff --git a/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
index e81d4e0608..2af881bda9 100644
--- a/include/hw/usb/redirect-server.h
+++ b/include/hw/usb/redirect-server.h
@@ -11,8 +11,11 @@
#include "hw/core/sysbus.h"
#include "hw/usb/usb.h"
+#include "chardev/char-fe.h"
#include "qom/object.h"
+#include <usbredirparser.h>
+
#define TYPE_USB_REDIR_SERVER "usb-redir-server"
OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
@@ -22,6 +25,18 @@ struct USBRedirServer {
/* 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;
};
#endif /* HW_USB_REDIRECT_SERVER_H */
diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 5f365f0a35..c36b5c16c4 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -50,10 +50,18 @@
*/
#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/usb/redirect-server.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/qdev-properties-system.h"
+#include "trace.h"
+
+#define USBREDIR_SERVER_VERSION "qemu " TYPE_USB_REDIR_SERVER " " QEMU_VERSION
/*
* USB port ops
@@ -81,6 +89,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
*/
@@ -89,14 +323,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.
@@ -112,8 +378,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.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev
2026-10-05 3:20 ` [PATCH v2 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
@ 2026-10-05 8:59 ` marcandre.lureau
0 siblings, 0 replies; 19+ messages in thread
From: marcandre.lureau @ 2026-10-05 8:59 UTC (permalink / raw)
To: Jamin Lin
Cc: clg, 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, 05 Oct 2026 03:20:27 +0000, Jamin Lin <jamin_lin@aspeedtech.com> wrote:
> 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.
>
> [...]
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
--
Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 4/8] hw/usb/redirect-server: Announce the exported device
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
` (2 preceding siblings ...)
2026-10-05 3:20 ` [PATCH v2 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-05 3:20 ` [PATCH v2 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
` (4 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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.
Wait a short time before sending device_connect. Firmware often toggles
the USB pull-up a few times while it starts its controller. If the host
starts to enumerate while the device is off, we cannot answer its first
request, and it only retries after a control timeout of several seconds.
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>
---
include/hw/usb/redirect-server.h | 2 +
hw/usb/redirect-server.c | 166 ++++++++++++++++++++++++++++++-
hw/usb/trace-events | 7 ++
3 files changed, 174 insertions(+), 1 deletion(-)
diff --git a/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
index 2af881bda9..a3d5084424 100644
--- a/include/hw/usb/redirect-server.h
+++ b/include/hw/usb/redirect-server.h
@@ -31,12 +31,14 @@ struct USBRedirServer {
/* usbredir over the chardev */
struct usbredirparser *parser;
+ QEMUTimer *announce_timer;
QEMUBH *chardev_close_bh;
const uint8_t *read_buf;
int read_buf_size;
bool in_write;
guint watch;
bool host_connected;
+ bool device_announced;
};
#endif /* HW_USB_REDIRECT_SERVER_H */
diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index c36b5c16c4..824cd274b8 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -56,6 +56,8 @@
#include "qemu/main-loop.h"
#include "qemu/module.h"
#include "migration/vmstate.h"
+#include "qemu/timer.h"
+#include "qemu/cutils.h"
#include "hw/usb/redirect-server.h"
#include "hw/core/qdev-properties.h"
#include "hw/core/qdev-properties-system.h"
@@ -63,10 +65,115 @@
#define USBREDIR_SERVER_VERSION "qemu " TYPE_USB_REDIR_SERVER " " QEMU_VERSION
+/* Wait this long after attach before we announce the device. */
+#define USBREDIR_SERVER_ANNOUNCE_DEBOUNCE_MS 10
+
+/*
+ * 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 = {
+ .speed = usbredir_server_speed(device),
+ };
+
+ if (s->device_announced) {
+ return;
+ }
+ s->device_announced = true;
+
+ /* 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_schedule_announce(USBRedirServer *s)
+{
+ USBDevice *device = usbredir_server_device(s);
+
+ if (!s->host_connected || s->device_announced || !device ||
+ !device->attached) {
+ return;
+ }
+
+ timer_mod(s->announce_timer,
+ qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+ USBREDIR_SERVER_ANNOUNCE_DEBOUNCE_MS);
+}
+
+static void usbredir_server_do_announce(void *opaque)
+{
+ USBRedirServer *s = opaque;
+ USBDevice *device = usbredir_server_device(s);
+
+ /* Only announce if the device is still attached and the host is here. */
+ if (s->host_connected && s->parser && device && device->attached) {
+ usbredir_server_announce_device(s);
+ }
+}
+
+static void usbredir_server_port_attach(USBPort *port)
+{
+ USBRedirServer *s = port->opaque;
+
+ trace_usbredir_server_attach();
+ usbredir_server_schedule_announce(s);
+}
+
+static void usbredir_server_port_detach(USBPort *port)
+{
+ USBRedirServer *s = port->opaque;
+
+ trace_usbredir_server_detach(s->device_announced);
+
+ timer_del(s->announce_timer);
+
+ 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. */
@@ -78,6 +185,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,
};
@@ -181,13 +290,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_schedule_announce(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. */
}
/*
@@ -211,6 +360,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);
@@ -238,7 +395,10 @@ static void usbredir_server_create_parser(USBRedirServer *s)
static void usbredir_server_destroy_parser(USBRedirServer *s)
{
s->host_connected = false;
+ s->device_announced = false;
+ /* The announce timer may still be pending. */
+ timer_del(s->announce_timer);
g_clear_handle_id(&s->watch, g_source_remove);
if (s->parser) {
@@ -336,6 +496,8 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL |
USB_SPEED_MASK_HIGH);
+ s->announce_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
+ usbredir_server_do_announce, s);
s->chardev_close_bh = qemu_bh_new_guarded(usbredir_server_chardev_close_bh,
s, &dev->mem_reentrancy_guard);
@@ -353,6 +515,8 @@ static void usbredir_server_unrealize(DeviceState *dev)
qemu_chr_fe_deinit(&s->cs, true);
usbredir_server_destroy_parser(s);
+ timer_free(s->announce_timer);
+
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 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.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 4/8] hw/usb/redirect-server: Announce the exported device
2026-10-05 3:20 ` [PATCH v2 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
@ 2026-10-05 8:59 ` marcandre.lureau
2026-10-07 7:32 ` Jamin Lin
0 siblings, 1 reply; 19+ messages in thread
From: marcandre.lureau @ 2026-10-05 8:59 UTC (permalink / raw)
To: Jamin Lin
Cc: clg, Peter Maydell, Steven Lee, Troy Lee, Kane Chen,
Andrew Jeffery, Joel Stanley, open list:ASPEED BMCs,
open list:All patches CC here, Troy Lee
> Send device_connect when a device is attached and the host has greeted
> us, device_disconnect when it goes away, and handle the bus reset the
> host sends before enumerating.
>
> Wait a short time before sending device_connect. Firmware often toggles
> the USB pull-up a few times while it starts its controller. If the host
> starts to enumerate while the device is off, we cannot answer its first
> request, and it only retries after a control timeout of several seconds.
sigh, this is going to hit us as machine may behave differently. I think
we should make the redirect-server machine-agnostic then.
--
Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 19+ messages in thread
* RE: [PATCH v2 4/8] hw/usb/redirect-server: Announce the exported device
2026-10-05 8:59 ` marcandre.lureau
@ 2026-10-07 7:32 ` Jamin Lin
0 siblings, 0 replies; 19+ messages in thread
From: Jamin Lin @ 2026-10-07 7:32 UTC (permalink / raw)
To: marcandre.lureau@redhat.com
Cc: clg@kaod.org, 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 v2 4/8] hw/usb/redirect-server: Announce the exported
> device
>
> > 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.
> >
> > Wait a short time before sending device_connect. Firmware often
> > toggles the USB pull-up a few times while it starts its controller. If
> > the host starts to enumerate while the device is off, we cannot answer
> > its first request, and it only retries after a control timeout of several seconds.
>
> sigh, this is going to hit us as machine may behave differently. I think we should
> make the redirect-server machine-agnostic then.
>
Thanks for the review and suggestion.
I added the 10 ms timer to avoid announcing the device before the
firmware had finished its USB initialization.
I went back and measured it. On both gadgets I tested, the port attaches
only once and does not detach during initialization:
Zephyr on AST1030, "usb enable": 1 attach, 0 detach
Linux on AST2600, gadget bind: 1 attach, 0 detach
So the delay is not needed for these cases.
I will drop the announce timer. When the device is attached, the redirect
server will send device_connect immediately. This keeps the redirect
server independent of machine-specific timing.
Thanks,
Jamin
> --
> Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 5/8] hw/usb/redirect-server: Implement control transfers
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
` (3 preceding siblings ...)
2026-10-05 3:20 ` [PATCH v2 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 3:20 ` [PATCH v2 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
` (3 subsequent siblings)
8 siblings, 0 replies; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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>
---
include/hw/usb/redirect-server.h | 46 +++
hw/usb/redirect-server.c | 603 ++++++++++++++++++++++++++++++-
hw/usb/trace-events | 5 +
3 files changed, 653 insertions(+), 1 deletion(-)
diff --git a/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
index a3d5084424..83c4524040 100644
--- a/include/hw/usb/redirect-server.h
+++ b/include/hw/usb/redirect-server.h
@@ -15,10 +15,46 @@
#include "qom/object.h"
#include <usbredirparser.h>
+#include <usbredirproto.h>
#define TYPE_USB_REDIR_SERVER "usb-redir-server"
OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
+/*
+ * 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;
@@ -39,6 +75,16 @@ 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];
};
#endif /* HW_USB_REDIRECT_SERVER_H */
diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 824cd274b8..5005ea5f8a 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -61,6 +61,7 @@
#include "hw/usb/redirect-server.h"
#include "hw/core/qdev-properties.h"
#include "hw/core/qdev-properties-system.h"
+#include "desc.h"
#include "trace.h"
#define USBREDIR_SERVER_VERSION "qemu " TYPE_USB_REDIR_SERVER " " QEMU_VERSION
@@ -68,6 +69,11 @@
/* Wait this long after attach before we announce the device. */
#define USBREDIR_SERVER_ANNOUNCE_DEBOUNCE_MS 10
+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.
@@ -78,8 +84,97 @@ 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)
{
@@ -119,6 +214,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);
+ }
}
/*
@@ -166,12 +401,20 @@ static void usbredir_server_port_detach(USBPort *port)
timer_del(s->announce_timer);
+ 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)
@@ -184,11 +427,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,
};
/*
@@ -198,6 +472,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
*/
@@ -313,9 +635,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();
@@ -339,6 +851,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
*/
@@ -361,13 +951,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);
@@ -401,6 +997,8 @@ static void usbredir_server_destroy_parser(USBRedirServer *s)
timer_del(s->announce_timer);
g_clear_handle_id(&s->watch, g_source_remove);
+ usbredir_server_stop_transfers(s);
+
if (s->parser) {
usbredirparser_destroy(s->parser);
s->parser = NULL;
@@ -489,6 +1087,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.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* [PATCH v2 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
` (4 preceding siblings ...)
2026-10-05 3:20 ` [PATCH v2 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 3:20 ` [PATCH v2 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
` (2 subsequent siblings)
8 siblings, 0 replies; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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>
---
include/hw/usb/redirect-server.h | 11 ++
hw/usb/redirect-server.c | 193 +++++++++++++++++++++++++++++++
hw/usb/trace-events | 5 +
3 files changed, 209 insertions(+)
diff --git a/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
index 83c4524040..f7b2261db4 100644
--- a/include/hw/usb/redirect-server.h
+++ b/include/hw/usb/redirect-server.h
@@ -9,6 +9,7 @@
#ifndef HW_USB_REDIRECT_SERVER_H
#define HW_USB_REDIRECT_SERVER_H
+#include "qemu/units.h"
#include "hw/core/sysbus.h"
#include "hw/usb/usb.h"
#include "chardev/char-fe.h"
@@ -27,8 +28,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 {
@@ -41,7 +50,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.
diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index 5005ea5f8a..a733a007e3 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -356,6 +356,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
*/
@@ -452,6 +498,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);
@@ -828,6 +880,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();
@@ -851,6 +1002,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 = {
@@ -899,7 +1068,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:
@@ -910,6 +1081,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;
}
@@ -952,6 +1138,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. */
@@ -963,7 +1151,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.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* [PATCH v2 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
` (5 preceding siblings ...)
2026-10-05 3:20 ` [PATCH v2 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 3:20 ` [PATCH v2 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
2026-10-05 8:59 ` [PATCH v2 0/8] hw/usb: Add a " marcandre.lureau
8 siblings, 0 replies; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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>
---
include/hw/usb/redirect-server.h | 15 ++
hw/usb/redirect-server.c | 248 +++++++++++++++++++++++++++++++
hw/usb/trace-events | 4 +
3 files changed, 267 insertions(+)
diff --git a/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
index f7b2261db4..1fb2a8a7a4 100644
--- a/include/hw/usb/redirect-server.h
+++ b/include/hw/usb/redirect-server.h
@@ -28,16 +28,23 @@ 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
+
#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 {
@@ -87,6 +94,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;
diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index a733a007e3..f84ece02e4 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -69,6 +69,13 @@
/* Wait this long after attach before we announce the device. */
#define USBREDIR_SERVER_ANNOUNCE_DEBOUNCE_MS 10
+/*
+ * 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
+
static void usbredir_server_pkt_free(USBRedirServerPkt *rp);
static void usbredir_server_stop_transfers(USBRedirServer *s);
static void usbredir_server_send_cancelled(USBRedirServer *s,
@@ -402,6 +409,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
*/
@@ -504,6 +568,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);
@@ -521,7 +588,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,
};
/*
@@ -572,6 +657,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
*/
@@ -1002,6 +1172,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)
{
@@ -1106,6 +1328,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.
@@ -1151,6 +1381,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 =
@@ -1292,6 +1526,10 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
s->announce_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
usbredir_server_do_announce, s);
+ 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);
@@ -1311,6 +1549,16 @@ static void usbredir_server_unrealize(DeviceState *dev)
timer_free(s->announce_timer);
+ 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.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* [PATCH v2 8/8] hw/arm/aspeed: Enable the usbredir server transport
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
` (6 preceding siblings ...)
2026-10-05 3:20 ` [PATCH v2 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
@ 2026-10-05 3:20 ` Jamin Lin
2026-10-05 8:59 ` [PATCH v2 0/8] hw/usb: Add a " marcandre.lureau
8 siblings, 0 replies; 19+ messages in thread
From: Jamin Lin @ 2026-10-05 3:20 UTC (permalink / raw)
To: clg@kaod.org, 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 "usb-redir-server" with -device, so a UDC
gadget can be exported to a USB host running in another QEMU:
-device usb-redir-server,id=redirsrv,chardev=<chr>
-device aspeed.udc-gadget,bus=redirsrv.0,udc=/machine/soc/udc
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 | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/hw/arm/aspeed.c b/hw/arm/aspeed.c
index a7cfbd6c5e..4e1ecbabe7 100644
--- a/hw/arm/aspeed.c
+++ b/hw/arm/aspeed.c
@@ -409,6 +409,12 @@ 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, "usb-redir-server");
amc->macs_mask = ASPEED_MAC0_ON;
amc->uart_default = ASPEED_DEV_UART5;
--
2.43.0
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 0/8] hw/usb: Add a usbredir server transport
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
` (7 preceding siblings ...)
2026-10-05 3:20 ` [PATCH v2 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
@ 2026-10-05 8:59 ` marcandre.lureau
2026-10-07 8:42 ` Jamin Lin
8 siblings, 1 reply; 19+ messages in thread
From: marcandre.lureau @ 2026-10-05 8:59 UTC (permalink / raw)
To: Jamin Lin
Cc: clg, 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
> 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
> =============
>
> 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 |
> 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
>
> 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 usb-redir-server makes one and acts as the host
That's a bit awkward, but makes sense. However I am not sure it sure it should
also require a machine and sit on the sysbus. We may also want to build a
specialized binary that doesn't cary any of the machine code etc and is
target-free. something like "qemu-usb", not necessarily with this series though
Patch 4 already works around firmware initialization.. I don't whether this
is acceptable.. Perhaps we can accept it for now, but I'd mark the device
"experimental" at this point.
> 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
> 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 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 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 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.
something I have long wished for, pretty nice
--
Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 19+ messages in thread* RE: [PATCH v2 0/8] hw/usb: Add a usbredir server transport
2026-10-05 8:59 ` [PATCH v2 0/8] hw/usb: Add a " marcandre.lureau
@ 2026-10-07 8:42 ` Jamin Lin
0 siblings, 0 replies; 19+ messages in thread
From: Jamin Lin @ 2026-10-07 8:42 UTC (permalink / raw)
To: marcandre.lureau@redhat.com
Cc: clg@kaod.org, 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 v2 0/8] hw/usb: Add a usbredir server transport
>
> Hi
>
> > 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
> > =============
> >
> > 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 |
> > 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
> >
> > 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 usb-redir-server makes one and acts as
> > the host
>
> That's a bit awkward, but makes sense. However I am not sure it sure it should
> also require a machine and sit on the sysbus. We may also want to build a
> specialized binary that doesn't cary any of the machine code etc and is
> target-free. something like "qemu-usb", not necessarily with this series though
>
I do not know yet how a "qemu-usb" binary would work, so v3 keeps the
sysbus design. I also did not find any other way to fix "Bus not found"
for "-device <dev>,bus=<id>.0". The lookup starts at the sysbus, so a
device that does not sit there cannot offer a bus.
Because of that I will mark the device experimental and rename the type
to "x-usb-redir-server". The file name stays redirect-server.c, like the
other x- types in the tree.
If you have a better idea, I am happy to try it.
> Patch 4 already works around firmware initialization.. I don't whether this is
> acceptable.. Perhaps we can accept it for now, but I'd mark the device
> "experimental" at this point.
>
That part is solved in v3, see my reply to patch 4
Thanks,
Jamin
> > 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 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 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 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 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.
>
> something I have long wished for, pretty nice
>
> --
> Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 19+ messages in thread