linux/drivers/usb/host/xhci-sideband.c
Wesley Cheng b85a2ebda1 usb: host: xhci: Notify xHCI sideband on transfer ring free
In the case of handling a USB bus reset, the xhci_discover_or_reset_device
can run without first notifying the xHCI sideband client driver to stop or
prevent the use of the transfer ring.  It was seen that when a bus reset
situation happened, the USB offload driver was attempting to fetch the xHCI
transfer ring information, which was already freed.

Tested-by: Puma Hsu <pumahsu@google.com>
Tested-by: Daehwan Jung <dh10.jung@samsung.com>
Signed-off-by: Wesley Cheng <quic_wcheng@quicinc.com>
Acked-by: Mark Brown <broonie@kernel.org>
Link: https://lore.kernel.org/r/20250409194804.3773260-6-quic_wcheng@quicinc.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
2025-04-11 13:02:30 +02:00

457 lines
12 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* xHCI host controller sideband support
*
* Copyright (c) 2023-2025, Intel Corporation.
*
* Author: Mathias Nyman
*/
#include <linux/usb/xhci-sideband.h>
#include <linux/dma-direct.h>
#include "xhci.h"
/* sideband internal helpers */
static struct sg_table *
xhci_ring_to_sgtable(struct xhci_sideband *sb, struct xhci_ring *ring)
{
struct xhci_segment *seg;
struct sg_table *sgt;
unsigned int n_pages;
struct page **pages;
struct device *dev;
size_t sz;
int i;
dev = xhci_to_hcd(sb->xhci)->self.sysdev;
sz = ring->num_segs * TRB_SEGMENT_SIZE;
n_pages = PAGE_ALIGN(sz) >> PAGE_SHIFT;
pages = kvmalloc_array(n_pages, sizeof(struct page *), GFP_KERNEL);
if (!pages)
return NULL;
sgt = kzalloc(sizeof(*sgt), GFP_KERNEL);
if (!sgt) {
kvfree(pages);
return NULL;
}
seg = ring->first_seg;
if (!seg)
goto err;
/*
* Rings can potentially have multiple segments, create an array that
* carries page references to allocated segments. Utilize the
* sg_alloc_table_from_pages() to create the sg table, and to ensure
* that page links are created.
*/
for (i = 0; i < ring->num_segs; i++) {
dma_get_sgtable(dev, sgt, seg->trbs, seg->dma,
TRB_SEGMENT_SIZE);
pages[i] = sg_page(sgt->sgl);
sg_free_table(sgt);
seg = seg->next;
}
if (sg_alloc_table_from_pages(sgt, pages, n_pages, 0, sz, GFP_KERNEL))
goto err;
/*
* Save first segment dma address to sg dma_address field for the sideband
* client to have access to the IOVA of the ring.
*/
sg_dma_address(sgt->sgl) = ring->first_seg->dma;
return sgt;
err:
kvfree(pages);
kfree(sgt);
return NULL;
}
static void
__xhci_sideband_remove_endpoint(struct xhci_sideband *sb, struct xhci_virt_ep *ep)
{
/*
* Issue a stop endpoint command when an endpoint is removed.
* The stop ep cmd handler will handle the ring cleanup.
*/
xhci_stop_endpoint_sync(sb->xhci, ep, 0, GFP_KERNEL);
ep->sideband = NULL;
sb->eps[ep->ep_index] = NULL;
}
/* sideband api functions */
/**
* xhci_sideband_notify_ep_ring_free - notify client of xfer ring free
* @sb: sideband instance for this usb device
* @ep_index: usb endpoint index
*
* Notifies the xHCI sideband client driver of a xHCI transfer ring free
* routine. This will allow for the client to ensure that all transfers
* are completed.
*
* The callback should be synchronous, as the ring free happens after.
*/
void xhci_sideband_notify_ep_ring_free(struct xhci_sideband *sb,
unsigned int ep_index)
{
struct xhci_sideband_event evt;
evt.type = XHCI_SIDEBAND_XFER_RING_FREE;
evt.evt_data = &ep_index;
if (sb->notify_client)
sb->notify_client(sb->intf, &evt);
}
EXPORT_SYMBOL_GPL(xhci_sideband_notify_ep_ring_free);
/**
* xhci_sideband_add_endpoint - add endpoint to sideband access list
* @sb: sideband instance for this usb device
* @host_ep: usb host endpoint
*
* Adds an endpoint to the list of sideband accessed endpoints for this usb
* device.
* After an endpoint is added the sideband client can get the endpoint transfer
* ring buffer by calling xhci_sideband_endpoint_buffer()
*
* Return: 0 on success, negative error otherwise.
*/
int
xhci_sideband_add_endpoint(struct xhci_sideband *sb,
struct usb_host_endpoint *host_ep)
{
struct xhci_virt_ep *ep;
unsigned int ep_index;
mutex_lock(&sb->mutex);
ep_index = xhci_get_endpoint_index(&host_ep->desc);
ep = &sb->vdev->eps[ep_index];
if (ep->ep_state & EP_HAS_STREAMS) {
mutex_unlock(&sb->mutex);
return -EINVAL;
}
/*
* Note, we don't know the DMA mask of the audio DSP device, if its
* smaller than for xhci it won't be able to access the endpoint ring
* buffer. This could be solved by not allowing the audio class driver
* to add the endpoint the normal way, but instead offload it immediately,
* and let this function add the endpoint and allocate the ring buffer
* with the smallest common DMA mask
*/
if (sb->eps[ep_index] || ep->sideband) {
mutex_unlock(&sb->mutex);
return -EBUSY;
}
ep->sideband = sb;
sb->eps[ep_index] = ep;
mutex_unlock(&sb->mutex);
return 0;
}
EXPORT_SYMBOL_GPL(xhci_sideband_add_endpoint);
/**
* xhci_sideband_remove_endpoint - remove endpoint from sideband access list
* @sb: sideband instance for this usb device
* @host_ep: usb host endpoint
*
* Removes an endpoint from the list of sideband accessed endpoints for this usb
* device.
* sideband client should no longer touch the endpoint transfer buffer after
* calling this.
*
* Return: 0 on success, negative error otherwise.
*/
int
xhci_sideband_remove_endpoint(struct xhci_sideband *sb,
struct usb_host_endpoint *host_ep)
{
struct xhci_virt_ep *ep;
unsigned int ep_index;
mutex_lock(&sb->mutex);
ep_index = xhci_get_endpoint_index(&host_ep->desc);
ep = sb->eps[ep_index];
if (!ep || !ep->sideband || ep->sideband != sb) {
mutex_unlock(&sb->mutex);
return -ENODEV;
}
__xhci_sideband_remove_endpoint(sb, ep);
xhci_initialize_ring_info(ep->ring);
mutex_unlock(&sb->mutex);
return 0;
}
EXPORT_SYMBOL_GPL(xhci_sideband_remove_endpoint);
int
xhci_sideband_stop_endpoint(struct xhci_sideband *sb,
struct usb_host_endpoint *host_ep)
{
struct xhci_virt_ep *ep;
unsigned int ep_index;
ep_index = xhci_get_endpoint_index(&host_ep->desc);
ep = sb->eps[ep_index];
if (!ep || !ep->sideband || ep->sideband != sb)
return -EINVAL;
return xhci_stop_endpoint_sync(sb->xhci, ep, 0, GFP_KERNEL);
}
EXPORT_SYMBOL_GPL(xhci_sideband_stop_endpoint);
/**
* xhci_sideband_get_endpoint_buffer - gets the endpoint transfer buffer address
* @sb: sideband instance for this usb device
* @host_ep: usb host endpoint
*
* Returns the address of the endpoint buffer where xHC controller reads queued
* transfer TRBs from. This is the starting address of the ringbuffer where the
* sideband client should write TRBs to.
*
* Caller needs to free the returned sg_table
*
* Return: struct sg_table * if successful. NULL otherwise.
*/
struct sg_table *
xhci_sideband_get_endpoint_buffer(struct xhci_sideband *sb,
struct usb_host_endpoint *host_ep)
{
struct xhci_virt_ep *ep;
unsigned int ep_index;
ep_index = xhci_get_endpoint_index(&host_ep->desc);
ep = sb->eps[ep_index];
if (!ep || !ep->ring || !ep->sideband || ep->sideband != sb)
return NULL;
return xhci_ring_to_sgtable(sb, ep->ring);
}
EXPORT_SYMBOL_GPL(xhci_sideband_get_endpoint_buffer);
/**
* xhci_sideband_get_event_buffer - return the event buffer for this device
* @sb: sideband instance for this usb device
*
* If a secondary xhci interupter is set up for this usb device then this
* function returns the address of the event buffer where xHC writes
* the transfer completion events.
*
* Caller needs to free the returned sg_table
*
* Return: struct sg_table * if successful. NULL otherwise.
*/
struct sg_table *
xhci_sideband_get_event_buffer(struct xhci_sideband *sb)
{
if (!sb || !sb->ir)
return NULL;
return xhci_ring_to_sgtable(sb, sb->ir->event_ring);
}
EXPORT_SYMBOL_GPL(xhci_sideband_get_event_buffer);
/**
* xhci_sideband_create_interrupter - creates a new interrupter for this sideband
* @sb: sideband instance for this usb device
* @num_seg: number of event ring segments to allocate
* @ip_autoclear: IP autoclearing support such as MSI implemented
*
* Sets up a xhci interrupter that can be used for this sideband accessed usb
* device. Transfer events for this device can be routed to this interrupters
* event ring by setting the 'Interrupter Target' field correctly when queueing
* the transfer TRBs.
* Once this interrupter is created the interrupter target ID can be obtained
* by calling xhci_sideband_interrupter_id()
*
* Returns 0 on success, negative error otherwise
*/
int
xhci_sideband_create_interrupter(struct xhci_sideband *sb, int num_seg,
bool ip_autoclear, u32 imod_interval, int intr_num)
{
int ret = 0;
if (!sb || !sb->xhci)
return -ENODEV;
mutex_lock(&sb->mutex);
if (sb->ir) {
ret = -EBUSY;
goto out;
}
sb->ir = xhci_create_secondary_interrupter(xhci_to_hcd(sb->xhci),
num_seg, imod_interval,
intr_num);
if (!sb->ir) {
ret = -ENOMEM;
goto out;
}
sb->ir->ip_autoclear = ip_autoclear;
out:
mutex_unlock(&sb->mutex);
return ret;
}
EXPORT_SYMBOL_GPL(xhci_sideband_create_interrupter);
/**
* xhci_sideband_remove_interrupter - remove the interrupter from a sideband
* @sb: sideband instance for this usb device
*
* Removes a registered interrupt for a sideband. This would allow for other
* sideband users to utilize this interrupter.
*/
void
xhci_sideband_remove_interrupter(struct xhci_sideband *sb)
{
if (!sb || !sb->ir)
return;
mutex_lock(&sb->mutex);
xhci_remove_secondary_interrupter(xhci_to_hcd(sb->xhci), sb->ir);
sb->ir = NULL;
mutex_unlock(&sb->mutex);
}
EXPORT_SYMBOL_GPL(xhci_sideband_remove_interrupter);
/**
* xhci_sideband_interrupter_id - return the interrupter target id
* @sb: sideband instance for this usb device
*
* If a secondary xhci interrupter is set up for this usb device then this
* function returns the ID used by the interrupter. The sideband client
* needs to write this ID to the 'Interrupter Target' field of the transfer TRBs
* it queues on the endpoints transfer ring to ensure transfer completion event
* are written by xHC to the correct interrupter event ring.
*
* Returns interrupter id on success, negative error othgerwise
*/
int
xhci_sideband_interrupter_id(struct xhci_sideband *sb)
{
if (!sb || !sb->ir)
return -ENODEV;
return sb->ir->intr_num;
}
EXPORT_SYMBOL_GPL(xhci_sideband_interrupter_id);
/**
* xhci_sideband_register - register a sideband for a usb device
* @intf: usb interface associated with the sideband device
*
* Allows for clients to utilize XHCI interrupters and fetch transfer and event
* ring parameters for executing data transfers.
*
* Return: pointer to a new xhci_sideband instance if successful. NULL otherwise.
*/
struct xhci_sideband *
xhci_sideband_register(struct usb_interface *intf, enum xhci_sideband_type type,
int (*notify_client)(struct usb_interface *intf,
struct xhci_sideband_event *evt))
{
struct usb_device *udev = interface_to_usbdev(intf);
struct usb_hcd *hcd = bus_to_hcd(udev->bus);
struct xhci_hcd *xhci = hcd_to_xhci(hcd);
struct xhci_virt_device *vdev;
struct xhci_sideband *sb;
/*
* Make sure the usb device is connected to a xhci controller. Fail
* registration if the type is anything other than XHCI_SIDEBAND_VENDOR,
* as this is the only type that is currently supported by xhci-sideband.
*/
if (!udev->slot_id || type != XHCI_SIDEBAND_VENDOR)
return NULL;
sb = kzalloc_node(sizeof(*sb), GFP_KERNEL, dev_to_node(hcd->self.sysdev));
if (!sb)
return NULL;
mutex_init(&sb->mutex);
/* check this device isn't already controlled via sideband */
spin_lock_irq(&xhci->lock);
vdev = xhci->devs[udev->slot_id];
if (!vdev || vdev->sideband) {
xhci_warn(xhci, "XHCI sideband for slot %d already in use\n",
udev->slot_id);
spin_unlock_irq(&xhci->lock);
kfree(sb);
return NULL;
}
sb->xhci = xhci;
sb->vdev = vdev;
sb->intf = intf;
sb->type = type;
sb->notify_client = notify_client;
vdev->sideband = sb;
spin_unlock_irq(&xhci->lock);
return sb;
}
EXPORT_SYMBOL_GPL(xhci_sideband_register);
/**
* xhci_sideband_unregister - unregister sideband access to a usb device
* @sb: sideband instance to be unregistered
*
* Unregisters sideband access to a usb device and frees the sideband
* instance.
* After this the endpoint and interrupter event buffers should no longer
* be accessed via sideband. The xhci driver can now take over handling
* the buffers.
*/
void
xhci_sideband_unregister(struct xhci_sideband *sb)
{
struct xhci_hcd *xhci;
int i;
if (!sb)
return;
xhci = sb->xhci;
mutex_lock(&sb->mutex);
for (i = 0; i < EP_CTX_PER_DEV; i++)
if (sb->eps[i])
__xhci_sideband_remove_endpoint(sb, sb->eps[i]);
mutex_unlock(&sb->mutex);
xhci_sideband_remove_interrupter(sb);
spin_lock_irq(&xhci->lock);
sb->xhci = NULL;
sb->vdev->sideband = NULL;
spin_unlock_irq(&xhci->lock);
kfree(sb);
}
EXPORT_SYMBOL_GPL(xhci_sideband_unregister);
MODULE_DESCRIPTION("xHCI sideband driver for secondary interrupter management");
MODULE_LICENSE("GPL");