* [PATCH 3/4] tests/qtest/usb-hcd-xhci: test isoch pacing with MFINDEX above 2^32
2026-09-21 5:46 [PATCH 0/4] hw/usb/hcd-xhci: two isoch fixes with qtests Junjie Cao
2026-09-21 5:46 ` [PATCH 1/4] hw/usb/hcd-xhci: fix interval alignment after MFINDEX passes 2^32 Junjie Cao
2026-09-21 5:46 ` [PATCH 2/4] hw/usb/hcd-xhci: don't assert on NAK when retrying an isoch transfer Junjie Cao
@ 2026-09-21 5:46 ` Junjie Cao
2026-09-22 4:57 ` Philippe Mathieu-Daudé
2026-09-21 5:46 ` [PATCH 4/4] tests/qtest/usb-hcd-xhci: test isoch endpoint type mismatch Junjie Cao
3 siblings, 1 reply; 8+ messages in thread
From: Junjie Cao @ 2026-09-21 5:46 UTC (permalink / raw)
To: qemu-devel
Cc: Thomas Huth, Philippe Mathieu-Daudé, Peter Maydell,
Gerd Hoffmann, Fabiano Rosas, Laurent Vivier, Paolo Bonzini,
Nicholas Piggin, Feifan Qian
Add the minimum needed to drive a transfer ring from the test. Use it to
queue an isoch TD with SIA on usb-audio after stepping the clock past
2^32 microframes, and check that the TD waits for the next interval
boundary. Before the ROUND_UP() change it completed as soon as the
doorbell was rung.
The machine is started with pit=off. With the i8254 present the clock
step takes 26s in an ASan build instead of well under a second.
Signed-off-by: Junjie Cao <junjie.cao@intel.com>
---
Notes:
Nick Piggin's v4 series has a fuller xhci ring harness for qtest:
https://lore.kernel.org/qemu-devel/20250502033047.102465-1-npiggin@gmail.com/
The helpers here are self-contained so that the fixes do not depend on
it. I can rebase onto that harness if it is revived.
tests/qtest/usb-hcd-xhci-test.c | 283 ++++++++++++++++++++++++++++++++
1 file changed, 283 insertions(+)
diff --git a/tests/qtest/usb-hcd-xhci-test.c b/tests/qtest/usb-hcd-xhci-test.c
index b58fa1e2da..61eca37b15 100644
--- a/tests/qtest/usb-hcd-xhci-test.c
+++ b/tests/qtest/usb-hcd-xhci-test.c
@@ -10,8 +10,68 @@
#include "qemu/osdep.h"
#include "libqtest-single.h"
#include "libqos/usb.h"
+#include "libqos/malloc-pc.h"
#include "qobject/qdict.h"
+/* capability registers */
+#define XHCI_CAPLENGTH 0x00
+#define XHCI_HCSPARAMS1 0x04
+#define XHCI_DBOFF 0x14
+#define XHCI_RTSOFF 0x18
+/* operational registers */
+#define XHCI_USBCMD 0x00
+#define XHCI_USBSTS 0x04
+#define XHCI_CRCR 0x18
+#define XHCI_DCBAAP 0x30
+#define XHCI_CONFIG 0x38
+#define XHCI_PORTSC(n) (0x400 + 0x10 * (n))
+/* interrupter 0, relative to the runtime registers */
+#define XHCI_ERSTSZ 0x28
+#define XHCI_ERSTBA 0x30
+#define XHCI_ERDP 0x38
+
+#define USBCMD_RS (1 << 0)
+#define USBCMD_HCRST (1 << 1)
+#define USBSTS_HCH (1 << 0)
+#define PORTSC_CCS (1 << 0)
+#define PORTSC_PR (1 << 4)
+#define PORTSC_PP (1 << 9)
+#define CRCR_RCS (1 << 0)
+#define ERDP_EHB (1 << 3)
+
+#define TRB_C (1 << 0)
+#define TRB_TR_IOC (1 << 5)
+#define TRB_TR_SIA (1U << 31)
+#define TRB_TYPE(t) ((t) << 10)
+#define TRB_GET_TYPE(control) (((control) >> 10) & 0x3f)
+#define TRB_GET_CCODE(status) ((status) >> 24)
+#define TRB_GET_SLOT(control) ((control) >> 24)
+
+#define TR_ISOCH 5
+#define CR_ENABLE_SLOT 9
+#define CR_ADDRESS_DEVICE 11
+#define CR_CONFIGURE_ENDPOINT 12
+#define ER_TRANSFER 32
+#define ER_COMMAND_COMPLETE 33
+#define CC_SUCCESS 1
+
+#define EP_TYPE_ISOCH_OUT 1
+#define EP_TYPE_CONTROL 4
+
+#define XHCI_RING_TRBS 64
+#define XHCI_MICROFRAME_NS 125000
+
+typedef struct XHCITest {
+ QTestState *qts;
+ QGuestAllocator alloc;
+ QPCIBus *bus;
+ struct qhc hc;
+ uint32_t oper, runtime, doorbell;
+ uint64_t cmd_ring, event_ring, input_ctx;
+ unsigned int cmd_idx, event_idx;
+ unsigned int port, slot;
+} XHCITest;
+
static void wait_device_deleted_event(QTestState *qtest, const char *id)
{
QDict *resp, *data;
@@ -109,6 +169,227 @@ static void test_usb_ccid_hotplug(void)
qtest_qmp_device_del(qts, "ccid");
}
+static uint32_t xhci_readl(XHCITest *x, uint32_t off)
+{
+ return qpci_io_readl(x->hc.dev, x->hc.bar, off);
+}
+
+static void xhci_writel(XHCITest *x, uint32_t off, uint32_t val)
+{
+ qpci_io_writel(x->hc.dev, x->hc.bar, off, val);
+}
+
+static void xhci_writeq(XHCITest *x, uint32_t off, uint64_t val)
+{
+ xhci_writel(x, off, val);
+ xhci_writel(x, off + 4, val >> 32);
+}
+
+static uint64_t xhci_alloc_page(XHCITest *x)
+{
+ uint64_t addr = guest_alloc(&x->alloc, 0x1000);
+
+ qtest_memset(x->qts, addr, 0, 0x1000);
+ return addr;
+}
+
+static void xhci_write_trb(XHCITest *x, uint64_t addr, uint64_t parameter,
+ uint32_t status, uint32_t control)
+{
+ qtest_writeq(x->qts, addr, parameter);
+ qtest_writel(x->qts, addr + 8, status);
+ qtest_writel(x->qts, addr + 12, control);
+}
+
+/* Fetch the next event if there is one. Does not advance the clock. */
+static bool xhci_next_event(XHCITest *x, uint32_t *status, uint32_t *control)
+{
+ uint64_t addr = x->event_ring + 16 * x->event_idx;
+ uint32_t c = qtest_readl(x->qts, addr + 12);
+
+ if (!(c & TRB_C)) {
+ return false;
+ }
+ if (status) {
+ *status = qtest_readl(x->qts, addr + 8);
+ }
+ if (control) {
+ *control = c;
+ }
+ x->event_idx++;
+ g_assert_cmpuint(x->event_idx, <, XHCI_RING_TRBS);
+ xhci_writeq(x, x->runtime + XHCI_ERDP, (addr + 16) | ERDP_EHB);
+ return true;
+}
+
+static unsigned int xhci_command(XHCITest *x, uint64_t parameter,
+ uint32_t control)
+{
+ uint32_t status;
+
+ g_assert_cmpuint(x->cmd_idx, <, XHCI_RING_TRBS);
+ xhci_write_trb(x, x->cmd_ring + 16 * x->cmd_idx++, parameter, 0,
+ control | TRB_C);
+ xhci_writel(x, x->doorbell, 0);
+
+ g_assert_true(xhci_next_event(x, &status, &control));
+ g_assert_cmpuint(TRB_GET_TYPE(control), ==, ER_COMMAND_COMPLETE);
+ g_assert_cmpuint(TRB_GET_CCODE(status), ==, CC_SUCCESS);
+ return TRB_GET_SLOT(control);
+}
+
+/*
+ * Start qemu-xhci with one USB device, run the controller and bring the
+ * device to the Addressed state.
+ */
+static void xhci_test_start(XHCITest *x, const char *usb_device)
+{
+ uint64_t dcbaa, erst, ep0_ring;
+ unsigned int maxports;
+
+ memset(x, 0, sizeof(*x));
+ /* pit=off: a long clock step would run the i8254 timer all the way */
+ x->qts = qtest_initf("-machine pc,pit=off -nodefaults "
+ "-device qemu-xhci,id=xhci,addr=04.0 %s", usb_device);
+ pc_alloc_init(&x->alloc, x->qts, ALLOC_NO_FLAGS);
+ x->bus = qpci_new_pc(x->qts, NULL);
+ qusb_pci_init_one(x->bus, &x->hc, QPCI_DEVFN(4, 0), 0);
+
+ x->oper = qpci_io_readb(x->hc.dev, x->hc.bar, XHCI_CAPLENGTH);
+ x->runtime = xhci_readl(x, XHCI_RTSOFF) & ~0x1f;
+ x->doorbell = xhci_readl(x, XHCI_DBOFF) & ~0x3;
+ maxports = xhci_readl(x, XHCI_HCSPARAMS1) >> 24;
+
+ xhci_writel(x, x->oper + XHCI_USBCMD, USBCMD_HCRST);
+ g_assert_false(xhci_readl(x, x->oper + XHCI_USBCMD) & USBCMD_HCRST);
+
+ dcbaa = xhci_alloc_page(x);
+ erst = xhci_alloc_page(x);
+ x->cmd_ring = xhci_alloc_page(x);
+ x->event_ring = xhci_alloc_page(x);
+ x->input_ctx = xhci_alloc_page(x);
+
+ xhci_writel(x, x->oper + XHCI_CONFIG, 1);
+ xhci_writeq(x, x->oper + XHCI_DCBAAP, dcbaa);
+ qtest_writeq(x->qts, erst, x->event_ring);
+ qtest_writel(x->qts, erst + 8, XHCI_RING_TRBS);
+ xhci_writel(x, x->runtime + XHCI_ERSTSZ, 1);
+ xhci_writeq(x, x->runtime + XHCI_ERSTBA, erst);
+ xhci_writeq(x, x->runtime + XHCI_ERDP, x->event_ring | ERDP_EHB);
+ xhci_writeq(x, x->oper + XHCI_CRCR, x->cmd_ring | CRCR_RCS);
+ xhci_writel(x, x->oper + XHCI_USBCMD, USBCMD_RS);
+ g_assert_false(xhci_readl(x, x->oper + XHCI_USBSTS) & USBSTS_HCH);
+
+ for (x->port = 0; x->port < maxports; x->port++) {
+ if (xhci_readl(x, x->oper + XHCI_PORTSC(x->port)) & PORTSC_CCS) {
+ break;
+ }
+ }
+ g_assert_cmpuint(x->port, <, maxports);
+ xhci_writel(x, x->oper + XHCI_PORTSC(x->port), PORTSC_PP | PORTSC_PR);
+ while (xhci_next_event(x, NULL, NULL)) {
+ /* drop the port status change events */
+ }
+
+ x->slot = xhci_command(x, 0, TRB_TYPE(CR_ENABLE_SLOT));
+ qtest_writeq(x->qts, dcbaa + 8 * x->slot, xhci_alloc_page(x));
+
+ /* input control context: add slot and ep0 */
+ qtest_writel(x->qts, x->input_ctx + 0x04, 0x3);
+ /* slot context: one context entry, root hub port */
+ qtest_writel(x->qts, x->input_ctx + 0x20, 1 << 27);
+ qtest_writel(x->qts, x->input_ctx + 0x24, (x->port + 1) << 16);
+ /* ep0 context */
+ ep0_ring = xhci_alloc_page(x);
+ qtest_writel(x->qts, x->input_ctx + 0x44,
+ (64 << 16) | (EP_TYPE_CONTROL << 3));
+ qtest_writeq(x->qts, x->input_ctx + 0x48, ep0_ring | 1);
+ xhci_command(x, x->input_ctx,
+ TRB_TYPE(CR_ADDRESS_DEVICE) | (x->slot << 24));
+}
+
+/* Returns the address of the transfer ring. */
+static uint64_t xhci_configure_ep(XHCITest *x, unsigned int epid,
+ unsigned int type, unsigned int interval,
+ unsigned int max_packet)
+{
+ uint64_t ring = xhci_alloc_page(x);
+ uint64_t epctx = x->input_ctx + 0x20 * (epid + 1);
+
+ qtest_memset(x->qts, x->input_ctx, 0, 0x1000);
+ qtest_writel(x->qts, x->input_ctx + 0x04, (1 << epid) | 1);
+ qtest_writel(x->qts, x->input_ctx + 0x20, epid << 27);
+ qtest_writel(x->qts, x->input_ctx + 0x24, (x->port + 1) << 16);
+ qtest_writel(x->qts, epctx + 0x00, interval << 16);
+ qtest_writel(x->qts, epctx + 0x04, (max_packet << 16) | (type << 3));
+ qtest_writeq(x->qts, epctx + 0x08, ring | 1);
+ xhci_command(x, x->input_ctx,
+ TRB_TYPE(CR_CONFIGURE_ENDPOINT) | (x->slot << 24));
+ return ring;
+}
+
+static void xhci_test_end(XHCITest *x)
+{
+ g_free(x->hc.dev);
+ qpci_free_pc(x->bus);
+ alloc_destroy(&x->alloc);
+ qtest_quit(x->qts);
+}
+
+static bool xhci_test_supported(const char *usb_device)
+{
+ const char *arch = qtest_get_arch();
+
+ if (strcmp(arch, "i386") != 0 && strcmp(arch, "x86_64") != 0) {
+ g_test_skip("Test only runs on x86 (pc machine)");
+ return false;
+ }
+ if (!qtest_has_device("qemu-xhci") || !qtest_has_device(usb_device)) {
+ g_test_skip("Devices not available");
+ return false;
+ }
+ return true;
+}
+
+/*
+ * An isoch TD with SIA set is run at the next interval boundary. That has to
+ * hold once the microframe index no longer fits in 32 bits as well.
+ */
+static void test_xhci_isoch_mfindex_32bit(void)
+{
+ const unsigned int interval = 6;
+ uint32_t control;
+ uint64_t ring;
+ XHCITest x;
+
+ if (!xhci_test_supported("usb-audio")) {
+ return;
+ }
+
+ xhci_test_start(&x, "-audiodev none,id=snd0 "
+ "-device usb-audio,audiodev=snd0");
+ ring = xhci_configure_ep(&x, 2, EP_TYPE_ISOCH_OUT, interval, 64);
+
+ /* Go past 2^32 microframes and stop off an interval boundary. */
+ qtest_clock_step(x.qts, (1ULL << 32) * XHCI_MICROFRAME_NS);
+ qtest_clock_step(x.qts, 5 * XHCI_MICROFRAME_NS);
+
+ xhci_write_trb(&x, ring, xhci_alloc_page(&x), 64,
+ TRB_TYPE(TR_ISOCH) | TRB_TR_SIA | TRB_TR_IOC | TRB_C);
+ xhci_writel(&x, x.doorbell + 4 * x.slot, 2);
+ g_assert_false(xhci_next_event(&x, NULL, NULL));
+
+ /*
+ * The streaming interface has not been enabled, so usb-audio stalls the
+ * TD. What matters is when that happens.
+ */
+ qtest_clock_step(x.qts, XHCI_MICROFRAME_NS << interval);
+ g_assert_true(xhci_next_event(&x, NULL, &control));
+ g_assert_cmpuint(TRB_GET_TYPE(control), ==, ER_TRANSFER);
+
+ xhci_test_end(&x);
+}
+
int main(int argc, char **argv)
{
int ret;
@@ -123,6 +404,8 @@ int main(int argc, char **argv)
if (qtest_has_device("usb-ccid")) {
qtest_add_func("/xhci/pci/hotplug/usb-ccid", test_usb_ccid_hotplug);
}
+ qtest_add_func("/xhci/pci/isoch/mfindex-32bit",
+ test_xhci_isoch_mfindex_32bit);
qtest_start("-device nec-usb-xhci,id=xhci"
" -drive id=drive0,if=none,file=null-co://,"
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 4/4] tests/qtest/usb-hcd-xhci: test isoch endpoint type mismatch
2026-09-21 5:46 [PATCH 0/4] hw/usb/hcd-xhci: two isoch fixes with qtests Junjie Cao
` (2 preceding siblings ...)
2026-09-21 5:46 ` [PATCH 3/4] tests/qtest/usb-hcd-xhci: test isoch pacing with MFINDEX above 2^32 Junjie Cao
@ 2026-09-21 5:46 ` Junjie Cao
3 siblings, 0 replies; 8+ messages in thread
From: Junjie Cao @ 2026-09-21 5:46 UTC (permalink / raw)
To: qemu-devel
Cc: Thomas Huth, Philippe Mathieu-Daudé, Peter Maydell,
Gerd Hoffmann, Fabiano Rosas, Laurent Vivier, Paolo Bonzini,
Nicholas Piggin, Feifan Qian
Configure the interrupt IN endpoint of usb-kbd as Isoch IN and queue one
TD. The TD is deferred by a microframe, the kick timer retries it,
usb-kbd NAKs, and a second doorbell retries it again. The TD has to stay
pending both times and the controller has to keep running.
Without the fix QEMU aborts in the timer retry. The endpoint setup is
taken from the reproducer attached to #3886.
Signed-off-by: Junjie Cao <junjie.cao@intel.com>
---
tests/qtest/usb-hcd-xhci-test.c | 35 +++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/tests/qtest/usb-hcd-xhci-test.c b/tests/qtest/usb-hcd-xhci-test.c
index 61eca37b15..2e173af750 100644
--- a/tests/qtest/usb-hcd-xhci-test.c
+++ b/tests/qtest/usb-hcd-xhci-test.c
@@ -33,6 +33,7 @@
#define USBCMD_RS (1 << 0)
#define USBCMD_HCRST (1 << 1)
#define USBSTS_HCH (1 << 0)
+#define USBSTS_HCE (1 << 12)
#define PORTSC_CCS (1 << 0)
#define PORTSC_PR (1 << 4)
#define PORTSC_PP (1 << 9)
@@ -57,6 +58,7 @@
#define EP_TYPE_ISOCH_OUT 1
#define EP_TYPE_CONTROL 4
+#define EP_TYPE_ISOCH_IN 5
#define XHCI_RING_TRBS 64
#define XHCI_MICROFRAME_NS 125000
@@ -390,6 +392,37 @@ static void test_xhci_isoch_mfindex_32bit(void)
xhci_test_end(&x);
}
+/*
+ * The endpoint type in the endpoint context is whatever the guest says. Tell
+ * the controller that the interrupt endpoint of usb-kbd is isoch. The idle
+ * keyboard NAKs, and the TD has to stay pending when first the kick timer and
+ * then a doorbell retry it.
+ */
+static void test_xhci_isoch_ep_type_mismatch(void)
+{
+ uint64_t ring;
+ XHCITest x;
+
+ if (!xhci_test_supported("usb-kbd")) {
+ return;
+ }
+
+ xhci_test_start(&x, "-device usb-kbd");
+ ring = xhci_configure_ep(&x, 3, EP_TYPE_ISOCH_IN, 0, 8);
+
+ xhci_write_trb(&x, ring, xhci_alloc_page(&x), 8,
+ TRB_TYPE(TR_ISOCH) | TRB_TR_SIA | TRB_TR_IOC | TRB_C);
+ xhci_writel(&x, x.doorbell + 4 * x.slot, 3);
+ qtest_clock_step(x.qts, 2 * XHCI_MICROFRAME_NS);
+ xhci_writel(&x, x.doorbell + 4 * x.slot, 3);
+
+ g_assert_false(xhci_next_event(&x, NULL, NULL));
+ g_assert_cmphex(xhci_readl(&x, x.oper + XHCI_USBSTS) &
+ (USBSTS_HCH | USBSTS_HCE), ==, 0);
+
+ xhci_test_end(&x);
+}
+
int main(int argc, char **argv)
{
int ret;
@@ -406,6 +439,8 @@ int main(int argc, char **argv)
}
qtest_add_func("/xhci/pci/isoch/mfindex-32bit",
test_xhci_isoch_mfindex_32bit);
+ qtest_add_func("/xhci/pci/isoch/ep-type-mismatch",
+ test_xhci_isoch_ep_type_mismatch);
qtest_start("-device nec-usb-xhci,id=xhci"
" -drive id=drive0,if=none,file=null-co://,"
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread