All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jamin Lin <jamin_lin@aspeedtech.com>
To: "berrange@redhat.com" <berrange@redhat.com>,
	"junjie.cao@linux.dev" <junjie.cao@linux.dev>,
	"kraxel@redhat.com" <kraxel@redhat.com>,
	"marcandre.lureau@redhat.com" <marcandre.lureau@redhat.com>,
	"clg@kaod.org" <clg@kaod.org>,
	"junjie.cao@intel.com" <junjie.cao@intel.com>,
	"Pierrick Bouvier" <pierrick.bouvier@oss.qualcomm.com>,
	"Peter Maydell" <peter.maydell@linaro.org>,
	"Kane Chen" <kane_chen@aspeedtech.com>,
	"Philippe Mathieu-Daudé" <philmd@oss.qualcomm.com>,
	"Tyrone Ting" <kfting@nuvoton.com>,
	"Hao Wu" <wuhaotsh@google.com>, "Zhao Liu" <zhao1.liu@intel.com>,
	"Richard Henderson" <richard.henderson@linaro.org>,
	"Helge Deller" <deller@gmx.de>,
	"Stefano Stabellini" <sstabellini@kernel.org>,
	"Anthony PERARD" <anthony@xenproject.org>,
	"Edgar E. Iglesias" <edgar.iglesias@gmail.com>,
	"open list:All patches CC here" <qemu-devel@nongnu.org>,
	"open list:ASPEED BMCs" <qemu-arm@nongnu.org>,
	"open list:X86 Xen CPUs" <xen-devel@lists.xenproject.org>
Cc: Jamin Lin <jamin_lin@aspeedtech.com>, Troy Lee <troy_lee@aspeedtech.com>
Subject: [PATCH v9 14/19] hw/usb/redirect-server: Implement control transfers
Date: Tue, 10 Nov 2026 07:28:39 +0000	[thread overview]
Message-ID: <20261110072818.2883349-15-jamin_lin@aspeedtech.com> (raw)
In-Reply-To: <20261110072818.2883349-1-jamin_lin@aspeedtech.com>

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

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

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

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

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

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

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

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

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


  parent reply	other threads:[~2026-11-10  7:29 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-10  7:28 [PATCH v9 00/19] hw/usb: Add a usbredir server transport Jamin Lin
2026-11-10  7:28 ` [PATCH v9 01/19] hw/usb/bus: Add an auto-hub flag to the USB bus Jamin Lin
2026-11-10  7:28 ` [PATCH v9 02/19] hw/usb/hcd-xhci: Add an auto-hub property and leave it off Jamin Lin
2026-11-10  7:28 ` [PATCH v9 03/19] hw/usb/hcd-ehci: " Jamin Lin
2026-11-10  7:28 ` [PATCH v9 04/19] hw/usb/hcd-uhci: " Jamin Lin
2026-11-10  7:28 ` [PATCH v9 05/19] hw/usb/hcd-ohci: " Jamin Lin
2026-11-10  7:28 ` [PATCH v9 06/19] hw/usb/hcd-dwc2: " Jamin Lin
2026-11-10  7:28 ` [PATCH v9 07/19] hw/usb/xen-usb: Do not insert an automatic hub Jamin Lin
2026-11-10  7:28 ` [PATCH v9 08/19] hw/arm/npcm7xx: Keep the " Jamin Lin
2026-11-10  7:28 ` [PATCH v9 09/19] hw/usb/bus: Turn off the automatic hub by default Jamin Lin
2026-11-10  7:28 ` [PATCH v9 10/19] docs/system/devices/usb: Note the automatic hub change Jamin Lin
2026-11-10  7:28 ` [PATCH v9 11/19] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
2026-11-10  7:28 ` [PATCH v9 12/19] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
2026-11-10  7:28 ` [PATCH v9 13/19] hw/usb/redirect-server: Announce the exported device Jamin Lin
2026-11-10  7:28 ` Jamin Lin [this message]
2026-11-10  7:28 ` [PATCH v9 15/19] hw/usb/redirect-server: Send device and interface information Jamin Lin
2026-11-10  7:28 ` [PATCH v9 16/19] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
2026-11-10  7:28 ` [PATCH v9 17/19] hw/usb/redirect-server: Stream interrupt IN endpoints Jamin Lin
2026-11-10  7:28 ` [PATCH v9 18/19] hw/usb/redirect-server: Implement isochronous OUT transfers Jamin Lin
2026-11-10  7:28 ` [PATCH v9 19/19] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261110072818.2883349-15-jamin_lin@aspeedtech.com \
    --to=jamin_lin@aspeedtech.com \
    --cc=anthony@xenproject.org \
    --cc=berrange@redhat.com \
    --cc=clg@kaod.org \
    --cc=deller@gmx.de \
    --cc=edgar.iglesias@gmail.com \
    --cc=junjie.cao@intel.com \
    --cc=junjie.cao@linux.dev \
    --cc=kane_chen@aspeedtech.com \
    --cc=kfting@nuvoton.com \
    --cc=kraxel@redhat.com \
    --cc=marcandre.lureau@redhat.com \
    --cc=peter.maydell@linaro.org \
    --cc=philmd@oss.qualcomm.com \
    --cc=pierrick.bouvier@oss.qualcomm.com \
    --cc=qemu-arm@nongnu.org \
    --cc=qemu-devel@nongnu.org \
    --cc=richard.henderson@linaro.org \
    --cc=sstabellini@kernel.org \
    --cc=troy_lee@aspeedtech.com \
    --cc=wuhaotsh@google.com \
    --cc=xen-devel@lists.xenproject.org \
    --cc=zhao1.liu@intel.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.