mirror of
https://github.com/torvalds/linux.git
synced 2026-03-07 23:24:35 +01:00
If the role change while we are suspended, the cdns3 driver switches to the
new mode during resume. However, switching to host mode in this context
causes a NULL pointer dereference.
The host role's start() operation registers a xhci-hcd device, but its
probe is deferred while we are in the resume path. The host role's resume()
operation assumes the xhci-hcd device is already probed, which is not the
case, leading to the dereference. Since the start() operation of the new
role is already called, the resume operation can be skipped.
So skip the resume operation for the new role if a role switch occurs
during resume. Once the resume sequence is complete, the xhci-hcd device
can be probed in case of host mode.
Unable to handle kernel NULL pointer dereference at virtual address 0000000000000208
Mem abort info:
...
Data abort info:
...
[0000000000000208] pgd=0000000000000000, p4d=0000000000000000
Internal error: Oops: 0000000096000004 [#1] SMP
Modules linked in:
CPU: 0 UID: 0 PID: 146 Comm: sh Not tainted
6.19.0-rc7-00013-g6e64f4aabfae-dirty #135 PREEMPT
Hardware name: Texas Instruments J7200 EVM (DT)
pstate: 20000005 (nzCv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--)
pc : usb_hcd_is_primary_hcd+0x0/0x1c
lr : cdns_host_resume+0x24/0x5c
...
Call trace:
usb_hcd_is_primary_hcd+0x0/0x1c (P)
cdns_resume+0x6c/0xbc
cdns3_controller_resume.isra.0+0xe8/0x17c
cdns3_plat_resume+0x18/0x24
platform_pm_resume+0x2c/0x68
dpm_run_callback+0x90/0x248
device_resume+0x100/0x24c
dpm_resume+0x190/0x2ec
dpm_resume_end+0x18/0x34
suspend_devices_and_enter+0x2b0/0xa44
pm_suspend+0x16c/0x5fc
state_store+0x80/0xec
kobj_attr_store+0x18/0x2c
sysfs_kf_write+0x7c/0x94
kernfs_fop_write_iter+0x130/0x1dc
vfs_write+0x240/0x370
ksys_write+0x70/0x108
__arm64_sys_write+0x1c/0x28
invoke_syscall+0x48/0x10c
el0_svc_common.constprop.0+0x40/0xe0
do_el0_svc+0x1c/0x28
el0_svc+0x34/0x108
el0t_64_sync_handler+0xa0/0xe4
el0t_64_sync+0x198/0x19c
Code: 52800003 f9407ca5 d63f00a0 17ffffe4 (f9410401)
---[ end trace 0000000000000000 ]---
Cc: stable <stable@kernel.org>
Fixes: 2cf2581cd2 ("usb: cdns3: add power lost support for system resume")
Signed-off-by: Thomas Richard (TI) <thomas.richard@bootlin.com>
Acked-by: Peter Chen <peter.chen@kernel.org>
Link: https://patch.msgid.link/20260130-usb-cdns3-fix-role-switching-during-resume-v1-1-44c456852b52@bootlin.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
580 lines
12 KiB
C
580 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Cadence USBSS and USBSSP DRD Driver.
|
|
*
|
|
* Copyright (C) 2018-2019 Cadence.
|
|
* Copyright (C) 2017-2018 NXP
|
|
* Copyright (C) 2019 Texas Instruments
|
|
*
|
|
* Author: Peter Chen <peter.chen@nxp.com>
|
|
* Pawel Laszczak <pawell@cadence.com>
|
|
* Roger Quadros <rogerq@ti.com>
|
|
*/
|
|
|
|
#include <linux/dma-mapping.h>
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/of.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/io.h>
|
|
#include <linux/pm_runtime.h>
|
|
|
|
#include "core.h"
|
|
#include "host-export.h"
|
|
#include "drd.h"
|
|
|
|
static int cdns_idle_init(struct cdns *cdns);
|
|
|
|
static int cdns_role_start(struct cdns *cdns, enum usb_role role)
|
|
{
|
|
int ret;
|
|
|
|
if (WARN_ON(role > USB_ROLE_DEVICE))
|
|
return 0;
|
|
|
|
mutex_lock(&cdns->mutex);
|
|
cdns->role = role;
|
|
mutex_unlock(&cdns->mutex);
|
|
|
|
if (!cdns->roles[role])
|
|
return -ENXIO;
|
|
|
|
if (cdns->roles[role]->state == CDNS_ROLE_STATE_ACTIVE)
|
|
return 0;
|
|
|
|
mutex_lock(&cdns->mutex);
|
|
ret = cdns->roles[role]->start(cdns);
|
|
if (!ret)
|
|
cdns->roles[role]->state = CDNS_ROLE_STATE_ACTIVE;
|
|
mutex_unlock(&cdns->mutex);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void cdns_role_stop(struct cdns *cdns)
|
|
{
|
|
enum usb_role role = cdns->role;
|
|
|
|
if (WARN_ON(role > USB_ROLE_DEVICE))
|
|
return;
|
|
|
|
if (cdns->roles[role]->state == CDNS_ROLE_STATE_INACTIVE)
|
|
return;
|
|
|
|
mutex_lock(&cdns->mutex);
|
|
cdns->roles[role]->stop(cdns);
|
|
cdns->roles[role]->state = CDNS_ROLE_STATE_INACTIVE;
|
|
mutex_unlock(&cdns->mutex);
|
|
}
|
|
|
|
static void cdns_exit_roles(struct cdns *cdns)
|
|
{
|
|
cdns_role_stop(cdns);
|
|
cdns_drd_exit(cdns);
|
|
}
|
|
|
|
/**
|
|
* cdns_core_init_role - initialize role of operation
|
|
* @cdns: Pointer to cdns structure
|
|
*
|
|
* Returns 0 on success otherwise negative errno
|
|
*/
|
|
static int cdns_core_init_role(struct cdns *cdns)
|
|
{
|
|
struct device *dev = cdns->dev;
|
|
enum usb_dr_mode best_dr_mode;
|
|
enum usb_dr_mode dr_mode;
|
|
int ret;
|
|
|
|
dr_mode = usb_get_dr_mode(dev);
|
|
cdns->role = USB_ROLE_NONE;
|
|
|
|
/*
|
|
* If driver can't read mode by means of usb_get_dr_mode function then
|
|
* chooses mode according with Kernel configuration. This setting
|
|
* can be restricted later depending on strap pin configuration.
|
|
*/
|
|
if (dr_mode == USB_DR_MODE_UNKNOWN) {
|
|
if (cdns->version == CDNSP_CONTROLLER_V2) {
|
|
if (IS_ENABLED(CONFIG_USB_CDNSP_HOST) &&
|
|
IS_ENABLED(CONFIG_USB_CDNSP_GADGET))
|
|
dr_mode = USB_DR_MODE_OTG;
|
|
else if (IS_ENABLED(CONFIG_USB_CDNSP_HOST))
|
|
dr_mode = USB_DR_MODE_HOST;
|
|
else if (IS_ENABLED(CONFIG_USB_CDNSP_GADGET))
|
|
dr_mode = USB_DR_MODE_PERIPHERAL;
|
|
} else {
|
|
if (IS_ENABLED(CONFIG_USB_CDNS3_HOST) &&
|
|
IS_ENABLED(CONFIG_USB_CDNS3_GADGET))
|
|
dr_mode = USB_DR_MODE_OTG;
|
|
else if (IS_ENABLED(CONFIG_USB_CDNS3_HOST))
|
|
dr_mode = USB_DR_MODE_HOST;
|
|
else if (IS_ENABLED(CONFIG_USB_CDNS3_GADGET))
|
|
dr_mode = USB_DR_MODE_PERIPHERAL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* At this point cdns->dr_mode contains strap configuration.
|
|
* Driver try update this setting considering kernel configuration
|
|
*/
|
|
best_dr_mode = cdns->dr_mode;
|
|
|
|
ret = cdns_idle_init(cdns);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (dr_mode == USB_DR_MODE_OTG) {
|
|
best_dr_mode = cdns->dr_mode;
|
|
} else if (cdns->dr_mode == USB_DR_MODE_OTG) {
|
|
best_dr_mode = dr_mode;
|
|
} else if (cdns->dr_mode != dr_mode) {
|
|
dev_err(dev, "Incorrect DRD configuration\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
dr_mode = best_dr_mode;
|
|
|
|
if (dr_mode == USB_DR_MODE_OTG || dr_mode == USB_DR_MODE_HOST) {
|
|
if ((cdns->version == CDNSP_CONTROLLER_V2 &&
|
|
IS_ENABLED(CONFIG_USB_CDNSP_HOST)) ||
|
|
(cdns->version < CDNSP_CONTROLLER_V2 &&
|
|
IS_ENABLED(CONFIG_USB_CDNS3_HOST)))
|
|
ret = cdns_host_init(cdns);
|
|
else
|
|
ret = -ENXIO;
|
|
|
|
if (ret) {
|
|
dev_err(dev, "Host initialization failed with %d\n",
|
|
ret);
|
|
goto err;
|
|
}
|
|
}
|
|
|
|
if (dr_mode == USB_DR_MODE_OTG || dr_mode == USB_DR_MODE_PERIPHERAL) {
|
|
if (cdns->gadget_init)
|
|
ret = cdns->gadget_init(cdns);
|
|
else
|
|
ret = -ENXIO;
|
|
|
|
if (ret) {
|
|
dev_err(dev, "Device initialization failed with %d\n",
|
|
ret);
|
|
goto err;
|
|
}
|
|
}
|
|
|
|
cdns->dr_mode = dr_mode;
|
|
|
|
ret = cdns_drd_update_mode(cdns);
|
|
if (ret)
|
|
goto err;
|
|
|
|
/* Initialize idle role to start with */
|
|
ret = cdns_role_start(cdns, USB_ROLE_NONE);
|
|
if (ret)
|
|
goto err;
|
|
|
|
switch (cdns->dr_mode) {
|
|
case USB_DR_MODE_OTG:
|
|
ret = cdns_hw_role_switch(cdns);
|
|
if (ret)
|
|
goto err;
|
|
break;
|
|
case USB_DR_MODE_PERIPHERAL:
|
|
ret = cdns_role_start(cdns, USB_ROLE_DEVICE);
|
|
if (ret)
|
|
goto err;
|
|
break;
|
|
case USB_DR_MODE_HOST:
|
|
ret = cdns_role_start(cdns, USB_ROLE_HOST);
|
|
if (ret)
|
|
goto err;
|
|
break;
|
|
default:
|
|
ret = -EINVAL;
|
|
goto err;
|
|
}
|
|
|
|
return 0;
|
|
err:
|
|
cdns_exit_roles(cdns);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* cdns_hw_role_state_machine - role switch state machine based on hw events.
|
|
* @cdns: Pointer to controller structure.
|
|
*
|
|
* Returns next role to be entered based on hw events.
|
|
*/
|
|
static enum usb_role cdns_hw_role_state_machine(struct cdns *cdns)
|
|
{
|
|
enum usb_role role = USB_ROLE_NONE;
|
|
int id, vbus;
|
|
|
|
if (cdns->dr_mode != USB_DR_MODE_OTG) {
|
|
if (cdns_is_host(cdns))
|
|
role = USB_ROLE_HOST;
|
|
if (cdns_is_device(cdns))
|
|
role = USB_ROLE_DEVICE;
|
|
|
|
return role;
|
|
}
|
|
|
|
id = cdns_get_id(cdns);
|
|
vbus = cdns_get_vbus(cdns);
|
|
|
|
/*
|
|
* Role change state machine
|
|
* Inputs: ID, VBUS
|
|
* Previous state: cdns->role
|
|
* Next state: role
|
|
*/
|
|
role = cdns->role;
|
|
|
|
switch (role) {
|
|
case USB_ROLE_NONE:
|
|
/*
|
|
* Driver treats USB_ROLE_NONE synonymous to IDLE state from
|
|
* controller specification.
|
|
*/
|
|
if (!id)
|
|
role = USB_ROLE_HOST;
|
|
else if (vbus)
|
|
role = USB_ROLE_DEVICE;
|
|
break;
|
|
case USB_ROLE_HOST: /* from HOST, we can only change to NONE */
|
|
if (id)
|
|
role = USB_ROLE_NONE;
|
|
break;
|
|
case USB_ROLE_DEVICE: /* from GADGET, we can only change to NONE*/
|
|
if (!vbus)
|
|
role = USB_ROLE_NONE;
|
|
break;
|
|
}
|
|
|
|
dev_dbg(cdns->dev, "role %d -> %d\n", cdns->role, role);
|
|
|
|
return role;
|
|
}
|
|
|
|
static int cdns_idle_role_start(struct cdns *cdns)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static void cdns_idle_role_stop(struct cdns *cdns)
|
|
{
|
|
/* Program Lane swap and bring PHY out of RESET */
|
|
phy_reset(cdns->usb3_phy);
|
|
}
|
|
|
|
static int cdns_idle_init(struct cdns *cdns)
|
|
{
|
|
struct cdns_role_driver *rdrv;
|
|
|
|
rdrv = devm_kzalloc(cdns->dev, sizeof(*rdrv), GFP_KERNEL);
|
|
if (!rdrv)
|
|
return -ENOMEM;
|
|
|
|
rdrv->start = cdns_idle_role_start;
|
|
rdrv->stop = cdns_idle_role_stop;
|
|
rdrv->state = CDNS_ROLE_STATE_INACTIVE;
|
|
rdrv->suspend = NULL;
|
|
rdrv->resume = NULL;
|
|
rdrv->name = "idle";
|
|
|
|
cdns->roles[USB_ROLE_NONE] = rdrv;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* cdns_hw_role_switch - switch roles based on HW state
|
|
* @cdns: controller
|
|
*/
|
|
int cdns_hw_role_switch(struct cdns *cdns)
|
|
{
|
|
enum usb_role real_role, current_role;
|
|
int ret = 0;
|
|
|
|
/* Depends on role switch class */
|
|
if (cdns->role_sw)
|
|
return 0;
|
|
|
|
pm_runtime_get_sync(cdns->dev);
|
|
|
|
current_role = cdns->role;
|
|
real_role = cdns_hw_role_state_machine(cdns);
|
|
|
|
/* Do nothing if nothing changed */
|
|
if (current_role == real_role)
|
|
goto exit;
|
|
|
|
cdns_role_stop(cdns);
|
|
|
|
dev_dbg(cdns->dev, "Switching role %d -> %d", current_role, real_role);
|
|
|
|
ret = cdns_role_start(cdns, real_role);
|
|
if (ret) {
|
|
/* Back to current role */
|
|
dev_err(cdns->dev, "set %d has failed, back to %d\n",
|
|
real_role, current_role);
|
|
ret = cdns_role_start(cdns, current_role);
|
|
if (ret)
|
|
dev_err(cdns->dev, "back to %d failed too\n",
|
|
current_role);
|
|
}
|
|
exit:
|
|
pm_runtime_put_sync(cdns->dev);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* cdns_role_get - get current role of controller.
|
|
*
|
|
* @sw: pointer to USB role switch structure
|
|
*
|
|
* Returns role
|
|
*/
|
|
static enum usb_role cdns_role_get(struct usb_role_switch *sw)
|
|
{
|
|
struct cdns *cdns = usb_role_switch_get_drvdata(sw);
|
|
|
|
return cdns->role;
|
|
}
|
|
|
|
/**
|
|
* cdns_role_set - set current role of controller.
|
|
*
|
|
* @sw: pointer to USB role switch structure
|
|
* @role: the previous role
|
|
* Handles below events:
|
|
* - Role switch for dual-role devices
|
|
* - USB_ROLE_GADGET <--> USB_ROLE_NONE for peripheral-only devices
|
|
*/
|
|
static int cdns_role_set(struct usb_role_switch *sw, enum usb_role role)
|
|
{
|
|
struct cdns *cdns = usb_role_switch_get_drvdata(sw);
|
|
int ret = 0;
|
|
|
|
pm_runtime_get_sync(cdns->dev);
|
|
|
|
if (cdns->role == role)
|
|
goto pm_put;
|
|
|
|
if (cdns->dr_mode == USB_DR_MODE_HOST) {
|
|
switch (role) {
|
|
case USB_ROLE_NONE:
|
|
case USB_ROLE_HOST:
|
|
break;
|
|
default:
|
|
goto pm_put;
|
|
}
|
|
}
|
|
|
|
if (cdns->dr_mode == USB_DR_MODE_PERIPHERAL) {
|
|
switch (role) {
|
|
case USB_ROLE_NONE:
|
|
case USB_ROLE_DEVICE:
|
|
break;
|
|
default:
|
|
goto pm_put;
|
|
}
|
|
}
|
|
|
|
cdns_role_stop(cdns);
|
|
ret = cdns_role_start(cdns, role);
|
|
if (ret)
|
|
dev_err(cdns->dev, "set role %d has failed\n", role);
|
|
|
|
pm_put:
|
|
pm_runtime_put_sync(cdns->dev);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* cdns_wakeup_irq - interrupt handler for wakeup events
|
|
* @irq: irq number for cdns3/cdnsp core device
|
|
* @data: structure of cdns
|
|
*
|
|
* Returns IRQ_HANDLED or IRQ_NONE
|
|
*/
|
|
static irqreturn_t cdns_wakeup_irq(int irq, void *data)
|
|
{
|
|
struct cdns *cdns = data;
|
|
|
|
if (cdns->in_lpm) {
|
|
disable_irq_nosync(irq);
|
|
cdns->wakeup_pending = true;
|
|
if ((cdns->role == USB_ROLE_HOST) && cdns->host_dev)
|
|
pm_request_resume(&cdns->host_dev->dev);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
return IRQ_NONE;
|
|
}
|
|
|
|
/**
|
|
* cdns_init - probe for cdns3/cdnsp core device
|
|
* @cdns: Pointer to cdns structure.
|
|
*
|
|
* Returns 0 on success otherwise negative errno
|
|
*/
|
|
int cdns_init(struct cdns *cdns)
|
|
{
|
|
struct device *dev = cdns->dev;
|
|
int ret;
|
|
|
|
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
|
|
if (ret) {
|
|
dev_err(dev, "error setting dma mask: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
mutex_init(&cdns->mutex);
|
|
|
|
if (device_property_read_bool(dev, "usb-role-switch")) {
|
|
struct usb_role_switch_desc sw_desc = { };
|
|
|
|
sw_desc.set = cdns_role_set;
|
|
sw_desc.get = cdns_role_get;
|
|
sw_desc.allow_userspace_control = true;
|
|
sw_desc.driver_data = cdns;
|
|
sw_desc.fwnode = dev->fwnode;
|
|
|
|
cdns->role_sw = usb_role_switch_register(dev, &sw_desc);
|
|
if (IS_ERR(cdns->role_sw)) {
|
|
dev_warn(dev, "Unable to register Role Switch\n");
|
|
return PTR_ERR(cdns->role_sw);
|
|
}
|
|
}
|
|
|
|
if (cdns->wakeup_irq) {
|
|
ret = devm_request_irq(cdns->dev, cdns->wakeup_irq,
|
|
cdns_wakeup_irq,
|
|
IRQF_SHARED,
|
|
dev_name(cdns->dev), cdns);
|
|
|
|
if (ret) {
|
|
dev_err(cdns->dev, "couldn't register wakeup irq handler\n");
|
|
goto role_switch_unregister;
|
|
}
|
|
}
|
|
|
|
ret = cdns_drd_init(cdns);
|
|
if (ret)
|
|
goto init_failed;
|
|
|
|
ret = cdns_core_init_role(cdns);
|
|
if (ret)
|
|
goto init_failed;
|
|
|
|
spin_lock_init(&cdns->lock);
|
|
|
|
dev_dbg(dev, "Cadence USB3 core: probe succeed\n");
|
|
|
|
return 0;
|
|
init_failed:
|
|
cdns_drd_exit(cdns);
|
|
role_switch_unregister:
|
|
if (cdns->role_sw)
|
|
usb_role_switch_unregister(cdns->role_sw);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cdns_init);
|
|
|
|
/**
|
|
* cdns_remove - unbind drd driver and clean up
|
|
* @cdns: Pointer to cdns structure.
|
|
*
|
|
* Returns 0 on success otherwise negative errno
|
|
*/
|
|
int cdns_remove(struct cdns *cdns)
|
|
{
|
|
cdns_exit_roles(cdns);
|
|
usb_role_switch_unregister(cdns->role_sw);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cdns_remove);
|
|
|
|
#ifdef CONFIG_PM_SLEEP
|
|
int cdns_suspend(struct cdns *cdns)
|
|
{
|
|
struct device *dev = cdns->dev;
|
|
unsigned long flags;
|
|
|
|
if (pm_runtime_status_suspended(dev))
|
|
pm_runtime_resume(dev);
|
|
|
|
if (cdns->roles[cdns->role]->suspend) {
|
|
spin_lock_irqsave(&cdns->lock, flags);
|
|
cdns->roles[cdns->role]->suspend(cdns, false);
|
|
spin_unlock_irqrestore(&cdns->lock, flags);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cdns_suspend);
|
|
|
|
int cdns_resume(struct cdns *cdns)
|
|
{
|
|
bool power_lost = cdns_power_is_lost(cdns);
|
|
enum usb_role real_role;
|
|
bool role_changed = false;
|
|
int ret = 0;
|
|
|
|
if (power_lost) {
|
|
if (!cdns->role_sw) {
|
|
real_role = cdns_hw_role_state_machine(cdns);
|
|
if (real_role != cdns->role) {
|
|
ret = cdns_hw_role_switch(cdns);
|
|
if (ret)
|
|
return ret;
|
|
role_changed = true;
|
|
}
|
|
}
|
|
|
|
if (!role_changed) {
|
|
if (cdns->role == USB_ROLE_HOST)
|
|
ret = cdns_drd_host_on(cdns);
|
|
else if (cdns->role == USB_ROLE_DEVICE)
|
|
ret = cdns_drd_gadget_on(cdns);
|
|
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (!role_changed && cdns->roles[cdns->role]->resume)
|
|
cdns->roles[cdns->role]->resume(cdns, power_lost);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cdns_resume);
|
|
|
|
void cdns_set_active(struct cdns *cdns, u8 set_active)
|
|
{
|
|
struct device *dev = cdns->dev;
|
|
|
|
if (set_active) {
|
|
pm_runtime_disable(dev);
|
|
pm_runtime_set_active(dev);
|
|
pm_runtime_enable(dev);
|
|
}
|
|
|
|
return;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cdns_set_active);
|
|
#endif /* CONFIG_PM_SLEEP */
|
|
|
|
MODULE_AUTHOR("Peter Chen <peter.chen@nxp.com>");
|
|
MODULE_AUTHOR("Pawel Laszczak <pawell@cadence.com>");
|
|
MODULE_AUTHOR("Roger Quadros <rogerq@ti.com>");
|
|
MODULE_DESCRIPTION("Cadence USBSS and USBSSP DRD Driver");
|
|
MODULE_LICENSE("GPL");
|