mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2025-08-05 16:54:27 +00:00

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>
457 lines
12 KiB
C
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");
|