[PATCH v6 2/4] usb: xhci: Allocate sideband ring segments with dma_alloc_coherent()

From: Wesley Cheng

Date: Mon Nov 09 2026 - 21:39:11 EST


Ring segments are normally allocated from a shared DMA pool sized and
aligned to TRB_SEGMENT_SIZE (4096 bytes). On kernels built with a
larger PAGE_SIZE (e.g. 16K or 64K page arches), multiple segments can
share the same physical page.

A sideband client that maps a ring buffer directly via the IOMMU
(which operates at page granularity) would then also expose any other
ring segment sharing that page, as well as the dma_pool bookkeeping
kept in free blocks, to the entity handling the offloaded endpoints.

Add a no_pool_alloc flag for endpoints and rings, which has the ring
segments allocated with dma_alloc_coherent() instead of from
xhci->segment_pool. Coherent allocations are page aligned, so each of
these segments gets a page of its own. When PAGE_SIZE is larger than
TRB_SEGMENT_SIZE, xhci_sideband_add_endpoint() sets the flag for the
endpoint, and xhci_sideband_create_interrupter() requests it for the
secondary event ring. Normal (non-offloaded) endpoints, and kernels
using 4K pages, are unaffected, as they keep allocating from
xhci->segment_pool.

xHCI still owns and frees these rings once the endpoint has been
dropped or the slot disabled. All of the ring free paths run in
sleepable context, except for xhci_free_dev(), which frees the virt
device while holding xhci->lock with interrupts disabled, where
dma_free_coherent() can't be used. Detach these rings while holding the
lock, after the slot is disabled, and free them once it is released.

Since xHCI never queues transfers to a sideband ring, refuse ring
expansion for these rings.

Suggested-by: Mathias Nyman <mathias.nyman@xxxxxxxxxxxxxxx>
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Wesley Cheng <wesley.cheng@xxxxxxxxxxxxxxxx>
---
drivers/usb/host/xhci-mem.c | 63 ++++++++++++++++++++++++++++++----------
drivers/usb/host/xhci-sideband.c | 9 +++++-
drivers/usb/host/xhci.c | 13 +++++++++
drivers/usb/host/xhci.h | 5 +++-
4 files changed, 73 insertions(+), 17 deletions(-)

diff --git a/drivers/usb/host/xhci-mem.c b/drivers/usb/host/xhci-mem.c
index 75577f441cdd..d280023ecbce 100644
--- a/drivers/usb/host/xhci-mem.c
+++ b/drivers/usb/host/xhci-mem.c
@@ -26,10 +26,15 @@
*
* Section 4.11.1.1:
* "All components of all Command and Transfer TRBs shall be initialized to '0'"
+ *
+ * If no_pool_alloc is set, the segment is allocated with dma_alloc_coherent()
+ * instead of from the segment pool. Coherent allocations are page aligned, so
+ * the segment then has a page of its own.
*/
static struct xhci_segment *xhci_segment_alloc(struct xhci_hcd *xhci,
unsigned int max_packet,
unsigned int num,
+ bool no_pool_alloc,
gfp_t flags)
{
struct xhci_segment *seg;
@@ -40,7 +45,11 @@ static struct xhci_segment *xhci_segment_alloc(struct xhci_hcd *xhci,
if (!seg)
return NULL;

- seg->trbs = dma_pool_zalloc(xhci->segment_pool, flags, &dma);
+ if (no_pool_alloc)
+ seg->trbs = dma_alloc_coherent(dev, TRB_SEGMENT_SIZE, &dma, flags);
+ else
+ seg->trbs = dma_pool_zalloc(xhci->segment_pool, flags, &dma);
+
if (!seg->trbs) {
kfree(seg);
return NULL;
@@ -50,7 +59,10 @@ static struct xhci_segment *xhci_segment_alloc(struct xhci_hcd *xhci,
seg->bounce_buf = kzalloc_node(max_packet, flags,
dev_to_node(dev));
if (!seg->bounce_buf) {
- dma_pool_free(xhci->segment_pool, seg->trbs, dma);
+ if (no_pool_alloc)
+ dma_free_coherent(dev, TRB_SEGMENT_SIZE, seg->trbs, dma);
+ else
+ dma_pool_free(xhci->segment_pool, seg->trbs, dma);
kfree(seg);
return NULL;
}
@@ -62,10 +74,15 @@ static struct xhci_segment *xhci_segment_alloc(struct xhci_hcd *xhci,
return seg;
}

-static void xhci_segment_free(struct xhci_hcd *xhci, struct xhci_segment *seg)
+static void xhci_segment_free(struct xhci_hcd *xhci, struct xhci_segment *seg,
+ bool no_pool_alloc)
{
if (seg->trbs) {
- dma_pool_free(xhci->segment_pool, seg->trbs, seg->dma);
+ if (no_pool_alloc)
+ dma_free_coherent(xhci_to_hcd(xhci)->self.sysdev,
+ TRB_SEGMENT_SIZE, seg->trbs, seg->dma);
+ else
+ dma_pool_free(xhci->segment_pool, seg->trbs, seg->dma);
seg->trbs = NULL;
}
kfree(seg->bounce_buf);
@@ -81,7 +98,7 @@ static void xhci_ring_segments_free(struct xhci_hcd *xhci, struct xhci_ring *rin

while (seg) {
next = seg->next;
- xhci_segment_free(xhci, seg);
+ xhci_segment_free(xhci, seg, ring->no_pool_alloc);
seg = next;
}
}
@@ -334,7 +351,7 @@ static int xhci_alloc_segments_for_ring(struct xhci_hcd *xhci, struct xhci_ring
struct xhci_segment *prev;
unsigned int num = 0;

- prev = xhci_segment_alloc(xhci, ring->bounce_buf_len, num, flags);
+ prev = xhci_segment_alloc(xhci, ring->bounce_buf_len, num, ring->no_pool_alloc, flags);
if (!prev)
return -ENOMEM;
num++;
@@ -343,7 +360,8 @@ static int xhci_alloc_segments_for_ring(struct xhci_hcd *xhci, struct xhci_ring
while (num < ring->num_segs) {
struct xhci_segment *next;

- next = xhci_segment_alloc(xhci, ring->bounce_buf_len, num, flags);
+ next = xhci_segment_alloc(xhci, ring->bounce_buf_len, num,
+ ring->no_pool_alloc, flags);
if (!next)
goto free_segments;

@@ -369,8 +387,9 @@ static int xhci_alloc_segments_for_ring(struct xhci_hcd *xhci, struct xhci_ring
* Set the end flag and the cycle toggle bit on the last segment.
* See section 4.9.1 and figures 15 and 16.
*/
-struct xhci_ring *xhci_ring_alloc(struct xhci_hcd *xhci, unsigned int num_segs,
- enum xhci_ring_type type, unsigned int max_packet, gfp_t flags)
+static struct xhci_ring *__xhci_ring_alloc(struct xhci_hcd *xhci, unsigned int num_segs,
+ enum xhci_ring_type type, unsigned int max_packet,
+ bool no_pool_alloc, gfp_t flags)
{
struct xhci_ring *ring;
int ret;
@@ -382,6 +401,7 @@ struct xhci_ring *xhci_ring_alloc(struct xhci_hcd *xhci, unsigned int num_segs,

ring->num_segs = num_segs;
ring->bounce_buf_len = max_packet;
+ ring->no_pool_alloc = no_pool_alloc;
INIT_LIST_HEAD(&ring->td_list);
ring->type = type;
if (num_segs == 0)
@@ -398,6 +418,12 @@ struct xhci_ring *xhci_ring_alloc(struct xhci_hcd *xhci, unsigned int num_segs,
return NULL;
}

+struct xhci_ring *xhci_ring_alloc(struct xhci_hcd *xhci, unsigned int num_segs,
+ enum xhci_ring_type type, unsigned int max_packet, gfp_t flags)
+{
+ return __xhci_ring_alloc(xhci, num_segs, type, max_packet, false, flags);
+}
+
void xhci_free_endpoint_ring(struct xhci_hcd *xhci,
struct xhci_virt_device *virt_dev,
unsigned int ep_index)
@@ -419,8 +445,12 @@ int xhci_ring_expansion(struct xhci_hcd *xhci, struct xhci_ring *ring,
if (num_new_segs == 0)
return 0;

+ if (ring->no_pool_alloc)
+ return -EINVAL;
+
new_ring.num_segs = num_new_segs;
new_ring.bounce_buf_len = ring->bounce_buf_len;
+ new_ring.no_pool_alloc = false;
new_ring.type = ring->type;
ret = xhci_alloc_segments_for_ring(xhci, &new_ring, flags);
if (ret)
@@ -1488,7 +1518,8 @@ int xhci_endpoint_init(struct xhci_hcd *xhci,

/* Set up the endpoint ring */
virt_dev->eps[ep_index].new_ring =
- xhci_ring_alloc(xhci, 2, ring_type, max_packet, mem_flags);
+ __xhci_ring_alloc(xhci, 2, ring_type, max_packet,
+ virt_dev->eps[ep_index].no_pool_alloc, mem_flags);
if (!virt_dev->eps[ep_index].new_ring)
return -ENOMEM;

@@ -2305,7 +2336,8 @@ static int xhci_setup_port_arrays(struct xhci_hcd *xhci, gfp_t flags)
}

static struct xhci_interrupter *
-xhci_alloc_interrupter(struct xhci_hcd *xhci, unsigned int segs, gfp_t flags)
+xhci_alloc_interrupter(struct xhci_hcd *xhci, unsigned int segs, bool no_pool_alloc,
+ gfp_t flags)
{
struct device *dev = xhci_to_hcd(xhci)->self.sysdev;
struct xhci_interrupter *ir;
@@ -2322,7 +2354,7 @@ xhci_alloc_interrupter(struct xhci_hcd *xhci, unsigned int segs, gfp_t flags)
if (!ir)
return NULL;

- ir->event_ring = xhci_ring_alloc(xhci, segs, TYPE_EVENT, 0, flags);
+ ir->event_ring = __xhci_ring_alloc(xhci, segs, TYPE_EVENT, 0, no_pool_alloc, flags);
if (!ir->event_ring) {
xhci_warn(xhci, "Failed to allocate interrupter event ring\n");
kfree(ir);
@@ -2370,7 +2402,8 @@ void xhci_add_interrupter(struct xhci_hcd *xhci, unsigned int intr_num)

struct xhci_interrupter *
xhci_create_secondary_interrupter(struct usb_hcd *hcd, unsigned int segs,
- u32 imod_interval, unsigned int intr_num)
+ u32 imod_interval, unsigned int intr_num,
+ bool no_pool_alloc)
{
struct xhci_hcd *xhci = hcd_to_xhci(hcd);
struct xhci_interrupter *ir;
@@ -2381,7 +2414,7 @@ xhci_create_secondary_interrupter(struct usb_hcd *hcd, unsigned int segs,
intr_num >= xhci->max_interrupters)
return NULL;

- ir = xhci_alloc_interrupter(xhci, segs, GFP_KERNEL);
+ ir = xhci_alloc_interrupter(xhci, segs, no_pool_alloc, GFP_KERNEL);
if (!ir)
return NULL;

@@ -2512,7 +2545,7 @@ int xhci_mem_init(struct xhci_hcd *xhci, gfp_t flags)
if (!xhci->interrupters)
goto fail;

- xhci->interrupters[0] = xhci_alloc_interrupter(xhci, 0, flags);
+ xhci->interrupters[0] = xhci_alloc_interrupter(xhci, 0, false, flags);
if (!xhci->interrupters[0])
goto fail;

diff --git a/drivers/usb/host/xhci-sideband.c b/drivers/usb/host/xhci-sideband.c
index d0c0f75d6085..a88698b085c7 100644
--- a/drivers/usb/host/xhci-sideband.c
+++ b/drivers/usb/host/xhci-sideband.c
@@ -98,6 +98,7 @@ __xhci_sideband_remove_endpoint(struct xhci_sideband *sb, struct xhci_virt_ep *e
xhci_stop_endpoint_sync(sb->xhci, ep, 0, GFP_KERNEL);

ep->sideband = NULL;
+ ep->no_pool_alloc = false;
sb->eps[ep->ep_index] = NULL;
}

@@ -181,6 +182,11 @@ xhci_sideband_add_endpoint(struct xhci_sideband *sb,
if (sb->eps[ep_index] || ep->sideband)
return -EBUSY;

+ /*
+ * The rings are mapped for the offload entity, so on kernels with pages
+ * larger than a ring segment, give each segment a page of its own.
+ */
+ ep->no_pool_alloc = PAGE_SIZE > TRB_SEGMENT_SIZE;
ep->sideband = sb;
sb->eps[ep_index] = ep;

@@ -347,7 +353,8 @@ xhci_sideband_create_interrupter(struct xhci_sideband *sb, int num_seg,

sb->ir = xhci_create_secondary_interrupter(xhci_to_hcd(sb->xhci),
num_seg, imod_interval,
- intr_num);
+ intr_num,
+ PAGE_SIZE > TRB_SEGMENT_SIZE);
if (!sb->ir)
return -ENOMEM;

diff --git a/drivers/usb/host/xhci.c b/drivers/usb/host/xhci.c
index fc1b0a6b0429..64710df97697 100644
--- a/drivers/usb/host/xhci.c
+++ b/drivers/usb/host/xhci.c
@@ -4106,6 +4106,7 @@ static int xhci_discover_or_reset_device(struct usb_hcd *hcd,
*/
static void xhci_free_dev(struct usb_hcd *hcd, struct usb_device *udev)
{
+ struct xhci_ring *no_pool_rings[EP_CTX_PER_DEV] = {};
struct xhci_hcd *xhci = hcd_to_xhci(hcd);
struct xhci_virt_device *virt_dev;
struct xhci_slot_ctx *slot_ctx;
@@ -4138,9 +4139,21 @@ static void xhci_free_dev(struct usb_hcd *hcd, struct usb_device *udev)
xhci_disable_slot(xhci, udev->slot_id);

spin_lock_irqsave(&xhci->lock, flags);
+ /*
+ * Rings not allocated from the segment pool are freed with
+ * dma_free_coherent(), which can't be called with interrupts disabled.
+ */
+ for (i = 0; i < EP_CTX_PER_DEV; i++) {
+ if (virt_dev->eps[i].ring && virt_dev->eps[i].ring->no_pool_alloc) {
+ no_pool_rings[i] = virt_dev->eps[i].ring;
+ virt_dev->eps[i].ring = NULL;
+ }
+ }
xhci_free_virt_device(xhci, virt_dev, udev->slot_id);
spin_unlock_irqrestore(&xhci->lock, flags);

+ for (i = 0; i < EP_CTX_PER_DEV; i++)
+ xhci_ring_free(xhci, no_pool_rings[i]);
}

int xhci_disable_slot(struct xhci_hcd *xhci, u32 slot_id)
diff --git a/drivers/usb/host/xhci.h b/drivers/usb/host/xhci.h
index ec4bfeb4887c..7e5152862562 100644
--- a/drivers/usb/host/xhci.h
+++ b/drivers/usb/host/xhci.h
@@ -711,6 +711,7 @@ struct xhci_virt_ep {
bool use_extended_tbc;
/* set if this endpoint is controlled via sideband access*/
struct xhci_sideband *sideband;
+ bool no_pool_alloc;
};

enum xhci_overhead_type {
@@ -1382,6 +1383,7 @@ struct xhci_ring {
unsigned int stream_id;
unsigned int num_segs;
unsigned int bounce_buf_len;
+ bool no_pool_alloc;
enum xhci_ring_type type;
u32 old_trb_comp_code;
struct radix_tree_root *trb_address_map;
@@ -1870,7 +1872,8 @@ void xhci_free_port_bw_ctx(struct xhci_hcd *xhci,
struct xhci_container_ctx *ctx);
struct xhci_interrupter *
xhci_create_secondary_interrupter(struct usb_hcd *hcd, unsigned int segs,
- u32 imod_interval, unsigned int intr_num);
+ u32 imod_interval, unsigned int intr_num,
+ bool no_pool_alloc);
void xhci_remove_secondary_interrupter(struct usb_hcd
*hcd, struct xhci_interrupter *ir);
void xhci_skip_sec_intr_events(struct xhci_hcd *xhci,

--
2.43.0