All of lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Krishna Kurapati" <krishna.kurapati@oss.qualcomm.com>
Cc: linux-phy@lists.infradead.org, devicetree@vger.kernel.org,
	robh@kernel.org, conor+dt@kernel.org, vkoul@kernel.org,
	neil.armstrong@linaro.org, olteanv@gmail.com
Subject: Re: [PATCH 2/2] phy: qcom: qusb2: Add charger detection support
Date: Wed, 11 Nov 2026 19:23:05 +0000	[thread overview]
Message-ID: <sashiko-outbox-209554@kernel.org> (raw)
In-Reply-To: <20261111-shikra-qusb2-charger-det-v1-2-5a9d919c66b5@oss.qualcomm.com>

Thank you for your contribution! Sashiko AI review found 10 potential
issues to consider:
- [High] phy: qcom: qusb2: unbalanced ref_clk accounting in
  qusb2_phy_init()
- [High] phy: qcom: qusb2: charger detection disables initialized PHY
- [High] phy: qcom: qusb2: PHY left enabled on USB_ROLE_NONE disconnect
- [High] phy: qcom: qusb2: missing teardown of work and role switches on
  unbind
- [High] phy: qcom: qusb2: missing Kconfig dependency on USB_ROLE_SWITCH
- [Medium] phy: qcom: qusb2: clock and regulator leak in
  qusb2_phy_init()
- [Medium] phy: qcom: qusb2: race between qusb2_role_switch_set() and
  work
- [Medium] phy: qcom: qusb2: mismatched register writes in
  qusb_phy_drive_dp_pulse()
- [Low] phy: qcom: qusb2: undocumented new members in struct qusb2_phy
  kernel-doc
- [Low] phy: qcom: qusb2: hardcoded charger name in
  qusb_phy_notify_charger()
--

commit 24edde049bf5427e2a864a633231dd8850451f54
Author: Krishna Kurapati <krishna.kurapati@oss.qualcomm.com>

phy: qcom: qusb2: Add charger detection support

This commit adds BC 1.2 charger detection to the QUSB2 PHY driver and
registers a USB role switch to intercept role transitions and forward
device mode to the controller only after detecting SDP or CDP.

> diff --git a/drivers/phy/qualcomm/phy-qcom-qusb2.c b/drivers/phy/qualcomm/phy-qcom-qusb2.c
> index 2e5fdb620cb2d..549dc8d1378a4 100644
> --- a/drivers/phy/qualcomm/phy-qcom-qusb2.c
> +++ b/drivers/phy/qualcomm/phy-qcom-qusb2.c
[ ... ]
> @@ -478,9 +536,425 @@ struct qusb2_phy {
>  	const struct qusb2_phy_cfg *cfg;
>  	bool has_se_clk_scheme;
>  	bool phy_initialized;
> +	bool is_phy_enabled;
>  	enum phy_mode mode;
> +	int vbus_active;
> +	int id_state;
> +	struct power_supply	*usb_psy;
> +	struct delayed_work	port_det_w;
> +	enum port_state		port_state;
> +	unsigned int		dcd_timeout;
> +	bool			dcp_charger;
> +
> +	struct usb_role_switch	*connector_role_sw;
> +	struct usb_role_switch	*controller_role_sw;
> +	enum usb_role		current_role;
> +	struct mutex		role_lock;
>  };

[Severity: Low]
struct qusb2_phy has a kernel-doc comment above it, so adding these 12
members without @member descriptions will trigger scripts/kernel-doc
warnings under W=1.

> +static int qusb_phy_notify_charger(struct qusb2_phy *qphy,
> +					enum power_supply_usb_type charger_type)
> +{
> +	union power_supply_propval pval = {0};
> +
> +	if (!qphy->usb_psy) {
> +		qphy->usb_psy = power_supply_get_by_name("pm4125-charger");
> +		if (!qphy->usb_psy) {
> +			dev_err(&qphy->phy->dev, "could not get usb power supply\n");
> +			return -ENODEV;
> +		}
> +	}

[Severity: Low]
Should this look up the charger power supply via a firmware reference
(such as devm_power_supply_get_by_reference()) instead of hardcoding
"pm4125-charger" in the generic QUSB2 PHY driver?  Any platform with
usb-role-switch enabled whose charger power supply uses a different name
will fail with -ENODEV here.

[ ... ]

> +static int qusb_phy_drive_dp_pulse(struct qusb2_phy *qphy)
> +{
> +	int ret;
> +
> +	dev_dbg(&qphy->phy->dev, "connected to a CDP, drive DP up\n");
> +
> +	ret = qusb2_phy_enable_phy(qphy);
> +	if (ret)
> +		return ret;
> +
> +	ret = reset_control_assert(qphy->phy_reset);
> +	if (ret)
> +		dev_err(&qphy->phy->dev, "phy assert failed\n");
> +	usleep_range(100, 150);
> +	ret = reset_control_deassert(qphy->phy_reset);
> +	if (ret)
> +		dev_err(&qphy->phy->dev, "phy deassert failed\n");
> +
> +	writel_relaxed(CLAMP_N_EN | FREEZIO_N | POWER_DOWN,
> +			qphy->base + qphy->cfg->regs[QUSB2PHY_PORT_POWERDOWN]);
> +	writel_relaxed(TERM_SELECT | XCVR_SELECT_FS | OP_MODE_NON_DRIVE |
> +			SUSPEND_N, qphy->base + QUSB2PHY_PORT_UTMI_CTRL1);
> +	writel_relaxed(UTMI_ULPI_SEL | UTMI_TEST_MUX_SEL,
> +			qphy->base + QUSB2PHY_PORT_UTMI_CTRL2);
> +	writel_relaxed(PLL_RESET_N_CNT_5,
> +			qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(CLAMP_N_EN | FREEZIO_N,
> +			qphy->base + qphy->cfg->regs[QUSB2PHY_PORT_POWERDOWN]);
> +	writel_relaxed(REF_BUF_EN | REXT_EN | PLL_BYPASSNL | REXT_TRIM_0,
> +			qphy->base + QUSB2PHY_PLL_PWR_CTRL);
> +
> +	usleep_range(5, 10);
> +
> +	writel_relaxed(0x15, qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(PLL_RESET_N | PLL_RESET_N_CNT_5,
> +			qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(0x00, qphy->base + QUSB2PHY_PORT_QC1);
> +	writel_relaxed(0x00, qphy->base + QUSB2PHY_PORT_QC2);
> +
> +	usleep_range(50, 60);
> +
> +	writel_relaxed(RDP_UP_EN, qphy->base + QUSB2PHY_PORT_QC2);
> +
> +	msleep(DP_PULSE_WIDTH_MSEC);
> +
> +	writel_relaxed(CLAMP_N_EN | FREEZIO_N | POWER_DOWN,
> +			qphy->base + qphy->cfg->regs[QUSB2PHY_PORT_POWERDOWN]);
> +	writel_relaxed(PLL_AUTOPGM_EN | PLL_RESET_N | PLL_RESET_N_CNT_5,
> +			qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(UTMI_ULPI_SEL, qphy->base + QUSB2PHY_PORT_UTMI_CTRL2);
> +	writel_relaxed(TERM_SELECT, qphy->base + QUSB2PHY_PORT_UTMI_CTRL1);
> +
> +	qusb2_phy_disable_phy(qphy);
> +
> +	return 0;
> +}

[Severity: Medium]
Are the register values and relaxed writes in qusb_phy_drive_dp_pulse()
intended?

The bring-up sequence in qusb_phy_chg_det_enable_seq() writes 0x17 to
QUSB2PHY_PLL_PWR_CTRL, whereas REF_BUF_EN | REXT_EN | PLL_BYPASSNL |
REXT_TRIM_0 evaluates to 0x53 because REF_BUF_EN is defined as BIT(6)
rather than BIT(2).  Below that, QUSB2PHY_PLL_AUTOPGM_CTL1 is written
twice with 0x15 (raw 0x15 followed by PLL_RESET_N | PLL_RESET_N_CNT_5),
and the four teardown writes at the end use writel_relaxed() without a
readback flush right before qusb2_phy_disable_phy() gates cfg_ahb_clk.

[ ... ]

> +	case PORT_PRIMARY_IN_PROGRESS:
> +		if (!qphy->vbus_active) {
> +			dev_dbg(&qphy->phy->dev, "VBUS lost during primary detection, aborting\n");
> +			qusb2_phy_disable_phy(qphy);
> +			qphy->port_state = PORT_DISCONNECTED;
> +			break;
> +		}
> +
> +		status = qusb_phy_chg_det_status(qphy, STATE_PRIMARY);
> +		if (status) {
> +			dev_dbg(&qphy->phy->dev,
> +				"primary detection positive, starting secondary detection\n");
> +			qusb_phy_chg_det_enable_seq(qphy, STATE_SECONDARY);
> +			qphy->port_state = PORT_SECONDARY_IN_PROGRESS;
> +			delay = CHG_SECONDARY_DET_TIME_MSEC;
> +
> +		} else {
> +			dev_dbg(&qphy->phy->dev, "charger detected: SDP (standard USB host)\n");
> +			qusb2_phy_disable_phy(qphy);
> +			qusb_phy_notify_charger(qphy, POWER_SUPPLY_USB_TYPE_SDP);
> +			qusb2_phy_forward_role(qphy, USB_ROLE_DEVICE);
> +			qphy->port_state = PORT_CHG_DET_DONE;
> +		}
> +		break;

[Severity: High]
What happens when charger detection runs after the USB controller has
already called phy_init()?

Controller drivers such as dwc3 call phy_init() -> qusb2_phy_init() during
probe, which sets qphy->is_phy_enabled = true and qphy->phy_initialized =
true.  When USB_ROLE_DEVICE later triggers qusb_phy_port_state_work(),
qusb_phy_chg_det_enable_seq() overwrites the operational PHY registers (and
qusb_phy_drive_dp_pulse() resets the PHY on CDP), and then
qusb2_phy_disable_phy() disables all clocks and regulators while
qphy->phy_initialized remains true.  Forwarding USB_ROLE_DEVICE then puts
the controller into device mode with a powered-down, unconfigured PHY, and
a subsequent qusb2_phy_runtime_suspend() or qusb2_phy_exit() will access
MMIO registers with cfg_ahb_clk and regulators disabled.

[ ... ]

> @@ -767,29 +1241,16 @@ static int qusb2_phy_init(struct phy *phy)
>  
>  	dev_vdbg(&phy->dev, "%s(): Initializing QUSB2 phy\n", __func__);
>  
> -	/* turn on regulator supplies */
> -	ret = regulator_bulk_enable(ARRAY_SIZE(qphy->vregs), qphy->vregs);
> +	/* Power up regulators and enable clocks (including ref_clk if needed). */
> +	ret = qusb2_phy_enable_phy(qphy);
>  	if (ret)
>  		return ret;
>  
> -	ret = clk_prepare_enable(qphy->iface_clk);
> -	if (ret) {
> -		dev_err(&phy->dev, "failed to enable iface_clk, %d\n", ret);
> -		goto poweroff_phy;
> -	}
> -
> -	/* enable ahb interface clock to program phy */
> -	ret = clk_prepare_enable(qphy->cfg_ahb_clk);
> -	if (ret) {
> -		dev_err(&phy->dev, "failed to enable cfg ahb clock, %d\n", ret);
> -		goto disable_iface_clk;
> -	}
> -
>  	/* Perform phy reset */
>  	ret = reset_control_assert(qphy->phy_reset);
>  	if (ret) {
>  		dev_err(&phy->dev, "failed to assert phy_reset, %d\n", ret);
> -		goto disable_ahb_clk;
> +		return ret;
>  	}

[Severity: Medium]
Does returning directly when reset_control_assert() fails leak the clocks
and regulators enabled by qusb2_phy_enable_phy() and leave
qphy->is_phy_enabled set to true?

>  	/* 100 us delay to keep PHY in reset mode */
>  	usleep_range(100, 150);
>  
>  	ret = reset_control_deassert(qphy->phy_reset);
>  	if (ret) {
>  		dev_err(&phy->dev, "failed to de-assert phy_reset, %d\n", ret);
> -		goto disable_ahb_clk;
> +		goto assert_phy_reset;
>  	}
>  
>  	/* Disable the PHY */
> @@ -887,23 +1348,15 @@ static int qusb2_phy_init(struct phy *phy)
>  		dev_err(&phy->dev,
>  			"QUSB2PHY pll lock failed: status reg = %x\n", val);
>  		ret = -EBUSY;
> -		goto disable_ref_clk;
> +		goto assert_phy_reset;
>  	}
>  	qphy->phy_initialized = true;
>  
>  	return 0;
>  
> -disable_ref_clk:
> -	if (!qphy->has_se_clk_scheme)
> -		clk_disable_unprepare(qphy->ref_clk);
>  assert_phy_reset:
>  	reset_control_assert(qphy->phy_reset);
> -disable_ahb_clk:
> -	clk_disable_unprepare(qphy->cfg_ahb_clk);
> -disable_iface_clk:
> -	clk_disable_unprepare(qphy->iface_clk);
> -poweroff_phy:
> -	regulator_bulk_disable(ARRAY_SIZE(qphy->vregs), qphy->vregs);
> +	qusb2_phy_disable_phy(qphy);
>  
>  	return ret;
>  }

[Severity: High]
Does moving clk_prepare_enable(qphy->ref_clk) into qusb2_phy_enable_phy()
unbalance ref_clk across both single-ended and differential clock
platforms?

qphy->has_se_clk_scheme starts as false and is only determined halfway
through qusb2_phy_init() from cfg->se_clk_scheme_default and qphy->tcsr:

drivers/phy/qualcomm/phy-qcom-qusb2.c:qusb2_phy_init() {
    ...
	qphy->has_se_clk_scheme = cfg->se_clk_scheme_default;
    ...
	if (!qphy->has_se_clk_scheme) {
		ret = clk_prepare_enable(qphy->ref_clk);
    ...
}

Because qusb2_phy_enable_phy() runs at the start of qusb2_phy_init() (or
earlier during charger detection) while qphy->has_se_clk_scheme is still
false, it always enables ref_clk.  On single-ended platforms,
qusb2_phy_init() then sets qphy->has_se_clk_scheme = true, so
qusb2_phy_exit() and qusb2_phy_disable_phy() both skip disabling ref_clk
and leak the enable reference.  On differential platforms,
qusb2_phy_init() still calls clk_prepare_enable(qphy->ref_clk) a second
time, leaving ref_clk enabled twice so qusb2_phy_runtime_suspend() cannot
gate it and the assert_phy_reset error path leaks one reference.

[ ... ]

> +static int qusb2_role_switch_set(struct usb_role_switch *sw, enum usb_role role)
> +{
[ ... ]
> +	switch (role) {
> +	case USB_ROLE_DEVICE:
> +		/*
> +		 * VBUS is active; start charger detection.  The state machine
> +		 * will forward USB_ROLE_DEVICE controller only for SDP/CDP.
> +		 */
> +		qphy->vbus_active = 1;
> +		qphy->id_state = 1;
> +		qphy->port_state = PORT_UNKNOWN;
> +		queue_delayed_work(system_freezable_wq, &qphy->port_det_w, 0);
> +		break;
> +	case USB_ROLE_HOST:
> +		qphy->id_state = 0;
> +		qphy->vbus_active = 0;
> +		qphy->port_state = PORT_UNKNOWN;
> +		queue_delayed_work(system_freezable_wq, &qphy->port_det_w, 0);
> +		break;
> +	case USB_ROLE_NONE:
> +	default:
> +		qphy->vbus_active = 0;
> +		qphy->id_state = 1;
> +		cancel_delayed_work(&qphy->port_det_w);
> +		qusb2_phy_forward_role(qphy, USB_ROLE_NONE);
> +		break;
> +	}
> +	mutex_unlock(&qphy->role_lock);
> +
> +	return 0;
> +}

[Severity: High]
Does canceling port_det_w on USB_ROLE_NONE prevent the disconnect cleanup
in qusb_phy_port_state_work() from ever running?

When a DCP charger is detected in PORT_SECONDARY_IN_PROGRESS, the PHY is
left enabled with qphy->dcp_charger = true, and PORT_CHG_DET_DONE returns
once while qphy->vbus_active is 1 without re-queuing port_det_w.  When the
cable is unplugged (either after DCP detection or mid-detection in
PORT_DCD_IN_PROGRESS / PORT_PRIMARY_IN_PROGRESS /
PORT_SECONDARY_IN_PROGRESS), USB_ROLE_NONE sets qphy->vbus_active = 0 and
calls cancel_delayed_work(&qphy->port_det_w) instead of running the state
machine or calling qusb2_phy_disable_phy(), leaving the PHY clocks,
regulators, and DP/DM detection circuits enabled and qphy->dcp_charger set
to true.

[Severity: Medium]
Can qusb2_role_switch_set() also race with an already-running
qusb_phy_port_state_work()?

CPU0                                      CPU1 (kworker)
qusb_phy_port_state_work()
  case PORT_SECONDARY_IN_PROGRESS:
    qusb_phy_drive_dp_pulse()
      msleep(DP_PULSE_WIDTH_MSEC)
qusb2_role_switch_set(USB_ROLE_NONE)
  mutex_lock(&qphy->role_lock)
  qphy->vbus_active = 0
  cancel_delayed_work(&qphy->port_det_w)
  qusb2_phy_forward_role(USB_ROLE_NONE)
  mutex_unlock(&qphy->role_lock)
                                          qusb2_phy_forward_role(USB_ROLE_DEVICE)
                                            <-- stale role forwarded after NONE
                                          qphy->port_state = PORT_CHG_DET_DONE

qusb_phy_port_state_work() does not acquire qphy->role_lock, and
cancel_delayed_work() does not wait for an executing work item to finish,
so a disconnect during the 200 ms sleep in qusb_phy_drive_dp_pulse() (or
in qusb_phy_chg_det_enable_seq()) allows the worker to overwrite
qphy->port_state and forward a stale USB_ROLE_DEVICE after USB_ROLE_NONE.

[ ... ]

> @@ -1131,7 +1651,29 @@ static int qusb2_phy_probe(struct platform_device *pdev)
>  
>  	phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
>  
> -	return PTR_ERR_OR_ZERO(phy_provider);
> +	if (IS_ERR(phy_provider))
> +		return PTR_ERR(phy_provider);
> +
> +	mutex_init(&qphy->role_lock);
> +	INIT_DELAYED_WORK(&qphy->port_det_w, qusb_phy_port_state_work);
> +
> +	if (device_property_read_bool(dev, "usb-role-switch")) {
> +		struct usb_role_switch_desc role_sw_desc = {};
> +
> +		role_sw_desc.fwnode = dev->fwnode;
> +		role_sw_desc.set = qusb2_role_switch_set;
> +		role_sw_desc.driver_data = qphy;
> +
> +		qphy->connector_role_sw = usb_role_switch_register(dev, &role_sw_desc);
> +		if (IS_ERR(qphy->connector_role_sw)) {
> +			ret = PTR_ERR(qphy->connector_role_sw);
> +			dev_err(dev, "failed to register role switch, %d\n", ret);
> +			return ret;
> +		}
> +	}
> +	qphy->controller_role_sw = NULL;
> +
> +	return 0;
>  }

[Severity: High]
Does PHY_QCOM_QUSB2 in drivers/phy/qualcomm/Kconfig need a dependency on
or selection of USB_ROLE_SWITCH?

With CONFIG_PHY_QCOM_QUSB2=y and CONFIG_USB_ROLE_SWITCH=m, vmlinux fails
to link with undefined references to usb_role_switch_register(),
usb_role_switch_get(), usb_role_switch_set_role(),
usb_role_switch_get_drvdata(), and usb_role_string().  In addition, when
CONFIG_USB_ROLE_SWITCH=n, the usb_role_switch_register() stub returns
ERR_PTR(-ENODEV), failing probe for any QUSB2 PHY node with the
usb-role-switch property.

[Severity: High]
What cleans up connector_role_sw, port_det_w, controller_role_sw, and
usb_psy when the driver is unbound?

qusb2_phy_driver has no .remove callback and registers no devm cleanup
actions for these resources.  Unbinding the device leaves
qphy->connector_role_sw registered and qphy->port_det_w queued or running
after devm frees qphy and unmaps qphy->base (causing a use-after-free),
and leaks the references acquired via usb_role_switch_get() and
power_supply_get_by_name().

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261111-shikra-qusb2-charger-det-v1-0-5a9d919c66b5@oss.qualcomm.com?part=2

WARNING: multiple messages have this Message-ID (diff)
From: sashiko-bot@kernel.org
To: "Krishna Kurapati" <krishna.kurapati@oss.qualcomm.com>
Cc: linux-phy@lists.infradead.org, devicetree@vger.kernel.org,
	robh@kernel.org, conor+dt@kernel.org, vkoul@kernel.org,
	neil.armstrong@linaro.org, olteanv@gmail.com
Subject: Re: [PATCH 2/2] phy: qcom: qusb2: Add charger detection support
Date: Wed, 11 Nov 2026 19:23:05 +0000	[thread overview]
Message-ID: <sashiko-outbox-209554@kernel.org> (raw)
In-Reply-To: <20261111-shikra-qusb2-charger-det-v1-2-5a9d919c66b5@oss.qualcomm.com>

Thank you for your contribution! Sashiko AI review found 10 potential
issues to consider:
- [High] phy: qcom: qusb2: unbalanced ref_clk accounting in
  qusb2_phy_init()
- [High] phy: qcom: qusb2: charger detection disables initialized PHY
- [High] phy: qcom: qusb2: PHY left enabled on USB_ROLE_NONE disconnect
- [High] phy: qcom: qusb2: missing teardown of work and role switches on
  unbind
- [High] phy: qcom: qusb2: missing Kconfig dependency on USB_ROLE_SWITCH
- [Medium] phy: qcom: qusb2: clock and regulator leak in
  qusb2_phy_init()
- [Medium] phy: qcom: qusb2: race between qusb2_role_switch_set() and
  work
- [Medium] phy: qcom: qusb2: mismatched register writes in
  qusb_phy_drive_dp_pulse()
- [Low] phy: qcom: qusb2: undocumented new members in struct qusb2_phy
  kernel-doc
- [Low] phy: qcom: qusb2: hardcoded charger name in
  qusb_phy_notify_charger()
--

commit 24edde049bf5427e2a864a633231dd8850451f54
Author: Krishna Kurapati <krishna.kurapati@oss.qualcomm.com>

phy: qcom: qusb2: Add charger detection support

This commit adds BC 1.2 charger detection to the QUSB2 PHY driver and
registers a USB role switch to intercept role transitions and forward
device mode to the controller only after detecting SDP or CDP.

> diff --git a/drivers/phy/qualcomm/phy-qcom-qusb2.c b/drivers/phy/qualcomm/phy-qcom-qusb2.c
> index 2e5fdb620cb2d..549dc8d1378a4 100644
> --- a/drivers/phy/qualcomm/phy-qcom-qusb2.c
> +++ b/drivers/phy/qualcomm/phy-qcom-qusb2.c
[ ... ]
> @@ -478,9 +536,425 @@ struct qusb2_phy {
>  	const struct qusb2_phy_cfg *cfg;
>  	bool has_se_clk_scheme;
>  	bool phy_initialized;
> +	bool is_phy_enabled;
>  	enum phy_mode mode;
> +	int vbus_active;
> +	int id_state;
> +	struct power_supply	*usb_psy;
> +	struct delayed_work	port_det_w;
> +	enum port_state		port_state;
> +	unsigned int		dcd_timeout;
> +	bool			dcp_charger;
> +
> +	struct usb_role_switch	*connector_role_sw;
> +	struct usb_role_switch	*controller_role_sw;
> +	enum usb_role		current_role;
> +	struct mutex		role_lock;
>  };

[Severity: Low]
struct qusb2_phy has a kernel-doc comment above it, so adding these 12
members without @member descriptions will trigger scripts/kernel-doc
warnings under W=1.

> +static int qusb_phy_notify_charger(struct qusb2_phy *qphy,
> +					enum power_supply_usb_type charger_type)
> +{
> +	union power_supply_propval pval = {0};
> +
> +	if (!qphy->usb_psy) {
> +		qphy->usb_psy = power_supply_get_by_name("pm4125-charger");
> +		if (!qphy->usb_psy) {
> +			dev_err(&qphy->phy->dev, "could not get usb power supply\n");
> +			return -ENODEV;
> +		}
> +	}

[Severity: Low]
Should this look up the charger power supply via a firmware reference
(such as devm_power_supply_get_by_reference()) instead of hardcoding
"pm4125-charger" in the generic QUSB2 PHY driver?  Any platform with
usb-role-switch enabled whose charger power supply uses a different name
will fail with -ENODEV here.

[ ... ]

> +static int qusb_phy_drive_dp_pulse(struct qusb2_phy *qphy)
> +{
> +	int ret;
> +
> +	dev_dbg(&qphy->phy->dev, "connected to a CDP, drive DP up\n");
> +
> +	ret = qusb2_phy_enable_phy(qphy);
> +	if (ret)
> +		return ret;
> +
> +	ret = reset_control_assert(qphy->phy_reset);
> +	if (ret)
> +		dev_err(&qphy->phy->dev, "phy assert failed\n");
> +	usleep_range(100, 150);
> +	ret = reset_control_deassert(qphy->phy_reset);
> +	if (ret)
> +		dev_err(&qphy->phy->dev, "phy deassert failed\n");
> +
> +	writel_relaxed(CLAMP_N_EN | FREEZIO_N | POWER_DOWN,
> +			qphy->base + qphy->cfg->regs[QUSB2PHY_PORT_POWERDOWN]);
> +	writel_relaxed(TERM_SELECT | XCVR_SELECT_FS | OP_MODE_NON_DRIVE |
> +			SUSPEND_N, qphy->base + QUSB2PHY_PORT_UTMI_CTRL1);
> +	writel_relaxed(UTMI_ULPI_SEL | UTMI_TEST_MUX_SEL,
> +			qphy->base + QUSB2PHY_PORT_UTMI_CTRL2);
> +	writel_relaxed(PLL_RESET_N_CNT_5,
> +			qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(CLAMP_N_EN | FREEZIO_N,
> +			qphy->base + qphy->cfg->regs[QUSB2PHY_PORT_POWERDOWN]);
> +	writel_relaxed(REF_BUF_EN | REXT_EN | PLL_BYPASSNL | REXT_TRIM_0,
> +			qphy->base + QUSB2PHY_PLL_PWR_CTRL);
> +
> +	usleep_range(5, 10);
> +
> +	writel_relaxed(0x15, qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(PLL_RESET_N | PLL_RESET_N_CNT_5,
> +			qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(0x00, qphy->base + QUSB2PHY_PORT_QC1);
> +	writel_relaxed(0x00, qphy->base + QUSB2PHY_PORT_QC2);
> +
> +	usleep_range(50, 60);
> +
> +	writel_relaxed(RDP_UP_EN, qphy->base + QUSB2PHY_PORT_QC2);
> +
> +	msleep(DP_PULSE_WIDTH_MSEC);
> +
> +	writel_relaxed(CLAMP_N_EN | FREEZIO_N | POWER_DOWN,
> +			qphy->base + qphy->cfg->regs[QUSB2PHY_PORT_POWERDOWN]);
> +	writel_relaxed(PLL_AUTOPGM_EN | PLL_RESET_N | PLL_RESET_N_CNT_5,
> +			qphy->base + QUSB2PHY_PLL_AUTOPGM_CTL1);
> +	writel_relaxed(UTMI_ULPI_SEL, qphy->base + QUSB2PHY_PORT_UTMI_CTRL2);
> +	writel_relaxed(TERM_SELECT, qphy->base + QUSB2PHY_PORT_UTMI_CTRL1);
> +
> +	qusb2_phy_disable_phy(qphy);
> +
> +	return 0;
> +}

[Severity: Medium]
Are the register values and relaxed writes in qusb_phy_drive_dp_pulse()
intended?

The bring-up sequence in qusb_phy_chg_det_enable_seq() writes 0x17 to
QUSB2PHY_PLL_PWR_CTRL, whereas REF_BUF_EN | REXT_EN | PLL_BYPASSNL |
REXT_TRIM_0 evaluates to 0x53 because REF_BUF_EN is defined as BIT(6)
rather than BIT(2).  Below that, QUSB2PHY_PLL_AUTOPGM_CTL1 is written
twice with 0x15 (raw 0x15 followed by PLL_RESET_N | PLL_RESET_N_CNT_5),
and the four teardown writes at the end use writel_relaxed() without a
readback flush right before qusb2_phy_disable_phy() gates cfg_ahb_clk.

[ ... ]

> +	case PORT_PRIMARY_IN_PROGRESS:
> +		if (!qphy->vbus_active) {
> +			dev_dbg(&qphy->phy->dev, "VBUS lost during primary detection, aborting\n");
> +			qusb2_phy_disable_phy(qphy);
> +			qphy->port_state = PORT_DISCONNECTED;
> +			break;
> +		}
> +
> +		status = qusb_phy_chg_det_status(qphy, STATE_PRIMARY);
> +		if (status) {
> +			dev_dbg(&qphy->phy->dev,
> +				"primary detection positive, starting secondary detection\n");
> +			qusb_phy_chg_det_enable_seq(qphy, STATE_SECONDARY);
> +			qphy->port_state = PORT_SECONDARY_IN_PROGRESS;
> +			delay = CHG_SECONDARY_DET_TIME_MSEC;
> +
> +		} else {
> +			dev_dbg(&qphy->phy->dev, "charger detected: SDP (standard USB host)\n");
> +			qusb2_phy_disable_phy(qphy);
> +			qusb_phy_notify_charger(qphy, POWER_SUPPLY_USB_TYPE_SDP);
> +			qusb2_phy_forward_role(qphy, USB_ROLE_DEVICE);
> +			qphy->port_state = PORT_CHG_DET_DONE;
> +		}
> +		break;

[Severity: High]
What happens when charger detection runs after the USB controller has
already called phy_init()?

Controller drivers such as dwc3 call phy_init() -> qusb2_phy_init() during
probe, which sets qphy->is_phy_enabled = true and qphy->phy_initialized =
true.  When USB_ROLE_DEVICE later triggers qusb_phy_port_state_work(),
qusb_phy_chg_det_enable_seq() overwrites the operational PHY registers (and
qusb_phy_drive_dp_pulse() resets the PHY on CDP), and then
qusb2_phy_disable_phy() disables all clocks and regulators while
qphy->phy_initialized remains true.  Forwarding USB_ROLE_DEVICE then puts
the controller into device mode with a powered-down, unconfigured PHY, and
a subsequent qusb2_phy_runtime_suspend() or qusb2_phy_exit() will access
MMIO registers with cfg_ahb_clk and regulators disabled.

[ ... ]

> @@ -767,29 +1241,16 @@ static int qusb2_phy_init(struct phy *phy)
>  
>  	dev_vdbg(&phy->dev, "%s(): Initializing QUSB2 phy\n", __func__);
>  
> -	/* turn on regulator supplies */
> -	ret = regulator_bulk_enable(ARRAY_SIZE(qphy->vregs), qphy->vregs);
> +	/* Power up regulators and enable clocks (including ref_clk if needed). */
> +	ret = qusb2_phy_enable_phy(qphy);
>  	if (ret)
>  		return ret;
>  
> -	ret = clk_prepare_enable(qphy->iface_clk);
> -	if (ret) {
> -		dev_err(&phy->dev, "failed to enable iface_clk, %d\n", ret);
> -		goto poweroff_phy;
> -	}
> -
> -	/* enable ahb interface clock to program phy */
> -	ret = clk_prepare_enable(qphy->cfg_ahb_clk);
> -	if (ret) {
> -		dev_err(&phy->dev, "failed to enable cfg ahb clock, %d\n", ret);
> -		goto disable_iface_clk;
> -	}
> -
>  	/* Perform phy reset */
>  	ret = reset_control_assert(qphy->phy_reset);
>  	if (ret) {
>  		dev_err(&phy->dev, "failed to assert phy_reset, %d\n", ret);
> -		goto disable_ahb_clk;
> +		return ret;
>  	}

[Severity: Medium]
Does returning directly when reset_control_assert() fails leak the clocks
and regulators enabled by qusb2_phy_enable_phy() and leave
qphy->is_phy_enabled set to true?

>  	/* 100 us delay to keep PHY in reset mode */
>  	usleep_range(100, 150);
>  
>  	ret = reset_control_deassert(qphy->phy_reset);
>  	if (ret) {
>  		dev_err(&phy->dev, "failed to de-assert phy_reset, %d\n", ret);
> -		goto disable_ahb_clk;
> +		goto assert_phy_reset;
>  	}
>  
>  	/* Disable the PHY */
> @@ -887,23 +1348,15 @@ static int qusb2_phy_init(struct phy *phy)
>  		dev_err(&phy->dev,
>  			"QUSB2PHY pll lock failed: status reg = %x\n", val);
>  		ret = -EBUSY;
> -		goto disable_ref_clk;
> +		goto assert_phy_reset;
>  	}
>  	qphy->phy_initialized = true;
>  
>  	return 0;
>  
> -disable_ref_clk:
> -	if (!qphy->has_se_clk_scheme)
> -		clk_disable_unprepare(qphy->ref_clk);
>  assert_phy_reset:
>  	reset_control_assert(qphy->phy_reset);
> -disable_ahb_clk:
> -	clk_disable_unprepare(qphy->cfg_ahb_clk);
> -disable_iface_clk:
> -	clk_disable_unprepare(qphy->iface_clk);
> -poweroff_phy:
> -	regulator_bulk_disable(ARRAY_SIZE(qphy->vregs), qphy->vregs);
> +	qusb2_phy_disable_phy(qphy);
>  
>  	return ret;
>  }

[Severity: High]
Does moving clk_prepare_enable(qphy->ref_clk) into qusb2_phy_enable_phy()
unbalance ref_clk across both single-ended and differential clock
platforms?

qphy->has_se_clk_scheme starts as false and is only determined halfway
through qusb2_phy_init() from cfg->se_clk_scheme_default and qphy->tcsr:

drivers/phy/qualcomm/phy-qcom-qusb2.c:qusb2_phy_init() {
    ...
	qphy->has_se_clk_scheme = cfg->se_clk_scheme_default;
    ...
	if (!qphy->has_se_clk_scheme) {
		ret = clk_prepare_enable(qphy->ref_clk);
    ...
}

Because qusb2_phy_enable_phy() runs at the start of qusb2_phy_init() (or
earlier during charger detection) while qphy->has_se_clk_scheme is still
false, it always enables ref_clk.  On single-ended platforms,
qusb2_phy_init() then sets qphy->has_se_clk_scheme = true, so
qusb2_phy_exit() and qusb2_phy_disable_phy() both skip disabling ref_clk
and leak the enable reference.  On differential platforms,
qusb2_phy_init() still calls clk_prepare_enable(qphy->ref_clk) a second
time, leaving ref_clk enabled twice so qusb2_phy_runtime_suspend() cannot
gate it and the assert_phy_reset error path leaks one reference.

[ ... ]

> +static int qusb2_role_switch_set(struct usb_role_switch *sw, enum usb_role role)
> +{
[ ... ]
> +	switch (role) {
> +	case USB_ROLE_DEVICE:
> +		/*
> +		 * VBUS is active; start charger detection.  The state machine
> +		 * will forward USB_ROLE_DEVICE controller only for SDP/CDP.
> +		 */
> +		qphy->vbus_active = 1;
> +		qphy->id_state = 1;
> +		qphy->port_state = PORT_UNKNOWN;
> +		queue_delayed_work(system_freezable_wq, &qphy->port_det_w, 0);
> +		break;
> +	case USB_ROLE_HOST:
> +		qphy->id_state = 0;
> +		qphy->vbus_active = 0;
> +		qphy->port_state = PORT_UNKNOWN;
> +		queue_delayed_work(system_freezable_wq, &qphy->port_det_w, 0);
> +		break;
> +	case USB_ROLE_NONE:
> +	default:
> +		qphy->vbus_active = 0;
> +		qphy->id_state = 1;
> +		cancel_delayed_work(&qphy->port_det_w);
> +		qusb2_phy_forward_role(qphy, USB_ROLE_NONE);
> +		break;
> +	}
> +	mutex_unlock(&qphy->role_lock);
> +
> +	return 0;
> +}

[Severity: High]
Does canceling port_det_w on USB_ROLE_NONE prevent the disconnect cleanup
in qusb_phy_port_state_work() from ever running?

When a DCP charger is detected in PORT_SECONDARY_IN_PROGRESS, the PHY is
left enabled with qphy->dcp_charger = true, and PORT_CHG_DET_DONE returns
once while qphy->vbus_active is 1 without re-queuing port_det_w.  When the
cable is unplugged (either after DCP detection or mid-detection in
PORT_DCD_IN_PROGRESS / PORT_PRIMARY_IN_PROGRESS /
PORT_SECONDARY_IN_PROGRESS), USB_ROLE_NONE sets qphy->vbus_active = 0 and
calls cancel_delayed_work(&qphy->port_det_w) instead of running the state
machine or calling qusb2_phy_disable_phy(), leaving the PHY clocks,
regulators, and DP/DM detection circuits enabled and qphy->dcp_charger set
to true.

[Severity: Medium]
Can qusb2_role_switch_set() also race with an already-running
qusb_phy_port_state_work()?

CPU0                                      CPU1 (kworker)
qusb_phy_port_state_work()
  case PORT_SECONDARY_IN_PROGRESS:
    qusb_phy_drive_dp_pulse()
      msleep(DP_PULSE_WIDTH_MSEC)
qusb2_role_switch_set(USB_ROLE_NONE)
  mutex_lock(&qphy->role_lock)
  qphy->vbus_active = 0
  cancel_delayed_work(&qphy->port_det_w)
  qusb2_phy_forward_role(USB_ROLE_NONE)
  mutex_unlock(&qphy->role_lock)
                                          qusb2_phy_forward_role(USB_ROLE_DEVICE)
                                            <-- stale role forwarded after NONE
                                          qphy->port_state = PORT_CHG_DET_DONE

qusb_phy_port_state_work() does not acquire qphy->role_lock, and
cancel_delayed_work() does not wait for an executing work item to finish,
so a disconnect during the 200 ms sleep in qusb_phy_drive_dp_pulse() (or
in qusb_phy_chg_det_enable_seq()) allows the worker to overwrite
qphy->port_state and forward a stale USB_ROLE_DEVICE after USB_ROLE_NONE.

[ ... ]

> @@ -1131,7 +1651,29 @@ static int qusb2_phy_probe(struct platform_device *pdev)
>  
>  	phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
>  
> -	return PTR_ERR_OR_ZERO(phy_provider);
> +	if (IS_ERR(phy_provider))
> +		return PTR_ERR(phy_provider);
> +
> +	mutex_init(&qphy->role_lock);
> +	INIT_DELAYED_WORK(&qphy->port_det_w, qusb_phy_port_state_work);
> +
> +	if (device_property_read_bool(dev, "usb-role-switch")) {
> +		struct usb_role_switch_desc role_sw_desc = {};
> +
> +		role_sw_desc.fwnode = dev->fwnode;
> +		role_sw_desc.set = qusb2_role_switch_set;
> +		role_sw_desc.driver_data = qphy;
> +
> +		qphy->connector_role_sw = usb_role_switch_register(dev, &role_sw_desc);
> +		if (IS_ERR(qphy->connector_role_sw)) {
> +			ret = PTR_ERR(qphy->connector_role_sw);
> +			dev_err(dev, "failed to register role switch, %d\n", ret);
> +			return ret;
> +		}
> +	}
> +	qphy->controller_role_sw = NULL;
> +
> +	return 0;
>  }

[Severity: High]
Does PHY_QCOM_QUSB2 in drivers/phy/qualcomm/Kconfig need a dependency on
or selection of USB_ROLE_SWITCH?

With CONFIG_PHY_QCOM_QUSB2=y and CONFIG_USB_ROLE_SWITCH=m, vmlinux fails
to link with undefined references to usb_role_switch_register(),
usb_role_switch_get(), usb_role_switch_set_role(),
usb_role_switch_get_drvdata(), and usb_role_string().  In addition, when
CONFIG_USB_ROLE_SWITCH=n, the usb_role_switch_register() stub returns
ERR_PTR(-ENODEV), failing probe for any QUSB2 PHY node with the
usb-role-switch property.

[Severity: High]
What cleans up connector_role_sw, port_det_w, controller_role_sw, and
usb_psy when the driver is unbound?

qusb2_phy_driver has no .remove callback and registers no devm cleanup
actions for these resources.  Unbinding the device leaves
qphy->connector_role_sw registered and qphy->port_det_w queued or running
after devm frees qphy and unmaps qphy->base (causing a use-after-free),
and leaks the references acquired via usb_role_switch_get() and
power_supply_get_by_name().

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261111-shikra-qusb2-charger-det-v1-0-5a9d919c66b5@oss.qualcomm.com?part=2

-- 
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy

  reply	other threads:[~2026-11-11 19:23 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-11 17:31 [PATCH 0/2] Add charger detection support for Shikra Krishna Kurapati
2026-11-11 17:31 ` Krishna Kurapati
2026-11-11 17:31 ` [PATCH 1/2] dt-bindings: phy: qcom,qusb2: Add support for remote endpoints Krishna Kurapati
2026-11-11 17:31   ` Krishna Kurapati
2026-11-11 19:03   ` sashiko-bot
2026-11-11 19:03     ` sashiko-bot
2026-11-11 17:31 ` [PATCH 2/2] phy: qcom: qusb2: Add charger detection support Krishna Kurapati
2026-11-11 17:31   ` Krishna Kurapati
2026-11-11 19:23   ` sashiko-bot [this message]
2026-11-11 19:23     ` sashiko-bot
2026-11-12  0:40   ` kernel test robot
2026-11-12  0:40     ` kernel test robot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-209554@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=krishna.kurapati@oss.qualcomm.com \
    --cc=linux-phy@lists.infradead.org \
    --cc=neil.armstrong@linaro.org \
    --cc=olteanv@gmail.com \
    --cc=robh@kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=vkoul@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.