mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: netdev-bot+sashiko@kernel.org
To: github@szelinsky.de
Cc: o.rempel@pengutronix.de, kory.maincent@bootlin.com,
	andrew+netdev@lunn.ch, hkallweit1@gmail.com,
	linux@armlinux.org.uk, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com, corey@leavitt.info,
	jelonek.jonas@gmail.com, horms@kernel.org, olek2@wp.pl,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH net-next v6 3/5] net: phy: own phydev->psec via PSE notifier and remove fwnode_mdio hook
Date: Wed, 09 Sep 2026 06:33:18 +0000	[thread overview]
Message-ID: <178893559852.219967.17408171091451392990@kernel.org> (raw)
In-Reply-To: <20260906153102.959217-4-github@szelinsky.de>

Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider:

Critical: 0 · High: 2 · Medium: 2 · Low: 2

- [High] Deferred psec put vs. bus-walk-based detach: moving the final
  pse_control_put(phydev->psec) from phy_device_remove() to…
- [High] The PSE_UNREGISTERED detach walk does not actually close the
  use-after-free class the commit claims to fix: the notifier callback…
- [Medium] pse_controller_unregister() frees the PI storage before
  quiescing the notification worker, and the new PSE_UNREGISTERED detach…
- [Medium] phy_try_attach_pse() treats every of_pse_control_get() error
  other than -ENOENT/-EPROBE_DEFER as permanent and warn-only, but that…
- [Low] The commit message claims that without the new PSE_UNREGISTERED
  detach walk "the PSE driver module could not finish unloading while…
- [Low] The patch fixes a reported, user-visible regression (boot-time
  probe-retry storm / PHYs failing to register when the PSE controller…

--

net: phy: own phydev->psec via PSE notifier and remove fwnode_mdio hook

This moves ownership of phydev->psec from fwnode_mdio into phylib, which
now subscribes to the pse-pd notifier chain and attaches/detaches psec on
PSE controller register/unregister events. phy_device_register() is split
into a public form that takes rtnl around the attach and a
phy_device_register_locked() form for callers already holding rtnl, and
the final pse_control_put() moves from phy_device_remove() to
phy_device_release().

Closes: https://lore.kernel.org/netdev/e00048dd-1ed3-40c3-9912-59bccf015ad5@gmail.com/

A couple of questions on the changelog first.

[Severity: Low]

>       - On PSE_UNREGISTERED: an rtnl-guarded bus walk releases every
>         phydev->psec that targets the departing controller before
>         pse_release_pis() frees pcdev->pi. Without this, a phy still
>         holding a pse_control reference would cause a use-after-free in
>         __pse_control_release()'s pcdev->pi[psec->id] access, and the PSE
>         driver module could not finish unloading while any phy still held a
>         reference.

Is the module unload part of this rationale the right way round?

pse_control_get_internal() pins the controller driver for every psec:

	if (!try_module_get(pcdev->owner)) {
		ret = -ENODEV;
		goto free_psec;
	}

and the matching module_put() only happens in __pse_control_release().
While a phy holds a psec the module refcount is non-zero, so
delete_module() fails with -EWOULDBLOCK before the module exit path (and
therefore pse_controller_unregister(), the only emitter of
PSE_UNREGISTERED) is ever entered.

The detach walk does help for driver unbind and device removal, where
pse_controller_unregister() is reached with the module still loaded, but
it cannot make an rmmod that was already refused succeed. Could that
sentence be reworded to talk about unbind/removal instead?

[Severity: Low]

>     Reported-by: Jonas Jelonek <jelonek.jonas@gmail.com>
>     Closes: https://lore.kernel.org/netdev/e00048dd-1ed3-40c3-9912-59bccf015ad5@gmail.com/

This carries Reported-by:, Closes: and Tested-by: for a user-visible
regression (PHYs failing to register / probe-retry storm with a modular
PSE controller driver), but there is no Fixes: tag and no indication of
the target tree.

git blame on the removed lookup in fwnode_mdiobus_register_phy() points at
5e82147de1cbd7 for the original PSE lookup, later touched by
fa2f0454174c2f, so the affected releases can be named. Could a Fixes: tag
be added, and could the intended tree (net vs net-next) be stated given
the size of the refactor and the new exported symbol?

> diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c
> index 0615228459ef4..f5febff4b00b8 100644
> --- a/drivers/net/phy/phy_device.c
> +++ b/drivers/net/phy/phy_device.c
> @@ -223,8 +223,19 @@ static void phy_mdio_device_free(struct mdio_device *mdiodev)
>  
>  static void phy_device_release(struct device *dev)
>  {
> +	struct phy_device *phydev = to_phy_device(dev);
> +
> +	/* bus_for_each_dev() holds get_device() across each iteration
> +	 * step, deferring this release callback until any in-flight PSE
> +	 * notifier walk has advanced past this phy. pse_control_put()
> +	 * takes pse_list_mutex, so this path must run in sleepable
> +	 * context.
> +	 */
> +	might_sleep();
> +	pse_control_put(phydev->psec);
> +
>  	fwnode_handle_put(dev->fwnode);
> -	kfree(to_phy_device(dev));
> +	kfree(phydev);
>  }

[Severity: High]

Can a psec survive here past the point where the detach walk can find it?

Once phy_device_remove() has run, device_del() -> bus_remove_device() ->
klist_del() takes the phy off the mdio_bus_type klist, but the phy itself
can still be pinned by a get_device() from phy_attach_direct() or
of_phy_find_device(). The detach is driven only by the klist walk:

	case PSE_UNREGISTERED:
		rtnl_lock();
		bus_for_each_dev(&mdio_bus_type, NULL, data,
				 phy_pse_detach_one);

so an off-bus phy keeps its psec, while pse_controller_unregister()
continues straight on:

	blocking_notifier_call_chain(&pse_controller_notifier,
				     PSE_UNREGISTERED, pcdev);
	pse_flush_pw_ds(pcdev);
	pse_release_pis(pcdev);		/* kfree(pcdev->pi) */

When the last device reference finally drops and this release callback
runs, __pse_control_release() does:

	if (psec->pcdev->pi[psec->id].admin_state_enabled)
		regulator_disable(psec->ps);

which reads the freed pi array and may act on it.

The last patch of this series ("net: phy: release phydev->psec from
phy_device_remove() again") restores the put plus phydev->psec = NULL
under pse_phy_lock() in phy_device_remove() before device_del(), which is
the ordering that avoids this. Would it be better to keep the put in
phy_device_remove() from this patch onwards so the intermediate tree is
not left with the window open?

> @@ -1102,11 +1113,103 @@ struct phy_device *get_phy_device(struct mii_bus *bus, int addr, bool is_c45)
>  }
>  EXPORT_SYMBOL(get_phy_device);
>  
> -/**
> - * phy_device_register - Register the phy device on the MDIO bus
> - * @phydev: phy_device structure to be added to the MDIO bus
> +/* Best-effort attach of phydev->psec from a DT `pses = <&...>` phandle.
> + * Caller must hold rtnl. A missing phandle (-ENOENT) or a not-yet-registered
> + * controller (-EPROBE_DEFER) is silent; the notifier retries the latter at
> + * PSE_REGISTERED time. Any other error means a broken binding and is warned
> + * about, but left non-fatal so the phy still registers.
>   */
> -int phy_device_register(struct phy_device *phydev)
> +static void phy_try_attach_pse(struct phy_device *phydev)
> +{
> +	struct pse_control *psec;
> +	struct device_node *np;
> +
> +	ASSERT_RTNL();
> +
> +	np = phydev->mdio.dev.of_node;
> +	if (!np)
> +		return;
> +
> +	if (phydev->psec)
> +		return;
> +
> +	psec = of_pse_control_get(np, phydev);
> +	if (IS_ERR(psec)) {
> +		if (PTR_ERR(psec) != -EPROBE_DEFER && PTR_ERR(psec) != -ENOENT)
> +			phydev_warn(phydev, "failed to get PSE control: %pe\n",
> +				    psec);
> +		return;
> +	}
> +
> +	phydev->psec = psec;
> +}

[Severity: Medium]

Is every error other than -ENOENT and -EPROBE_DEFER really a broken
binding? of_pse_control_get() does hardware traffic on this path:

drivers/net/pse-pd/pse_core.c:pse_control_get_internal() {
	...
	ret = pse_pi_is_hw_enabled(pcdev, index);
	if (ret < 0)
		goto free_psec;
	pcdev->pi[index].admin_state_enabled = ret;
	...
	psec->ps = devm_regulator_get_exclusive(...);
}

pse_pi_is_hw_enabled() calls pcdev->ops->pi_get_admin_state(), which is an
I2C/SMBus read on tps23881, si3474 and pd692x0, so a transient -EIO,
-EBUSY or -ENOMEM is possible. Here that is only warned about, phydev->psec
stays NULL, and the only retry left is a fresh PSE_REGISTERED event.

That is aggravated by PSE_REGISTERED firing synchronously from the tail of
pse_controller_register(), i.e. mid-probe of the controller driver:

drivers/net/pse-pd/tps23881.c:tps23881_probe() {
	...
	ret = devm_pse_controller_register(dev, &priv->pcdev);
	...
	ret = tps23881_setup_irq(priv, client->irq);
}

Previously the same failure propagated out of fwnode_mdiobus_register_phy()
and deferred probe retried it. With this change, does a single transient
bus error permanently lose the PSE binding for that port, leaving the PI
without a consumer and unreachable from ethtool even though
admin_state_enabled may have been seeded from the chip?

> +
> +static int phy_pse_attach_one(struct device *dev, void *data __maybe_unused)
> +{
> +	ASSERT_RTNL();
> +
> +	if (dev->type != &mdio_bus_phy_type)
> +		return 0;
> +
> +	phy_try_attach_pse(to_phy_device(dev));
> +	return 0;
> +}
> +
> +static int phy_pse_detach_one(struct device *dev, void *data)
> +{
> +	struct pse_controller_dev *pcdev = data;
> +	struct phy_device *phydev;
> +	struct pse_control *psec;
> +
> +	ASSERT_RTNL();
> +
> +	if (dev->type != &mdio_bus_phy_type)
> +		return 0;
> +
> +	phydev = to_phy_device(dev);
> +	psec = phydev->psec;
> +	if (!psec || !pse_control_matches_pcdev(psec, pcdev))
> +		return 0;
> +
> +	phydev->psec = NULL;
> +	pse_control_put(psec);
> +	return 0;
> +}

[Severity: Medium]

Once this walk drops the phy's reference, can the notification worker end
up being the last holder of the psec?

pse_send_ntf_worker() takes its own reference and then blocks on rtnl:

	psec = pse_control_find_by_id(pcdev, ntf.id);
	rtnl_lock();
	netdev = pse_control_get_netdev(psec);
	...
	rtnl_unlock();
	pse_control_put(psec);

The detach walk runs under rtnl, so it can complete in that window and the
psec then survives only on the worker's reference. Meanwhile
pse_controller_unregister() frees the PI storage before quiescing the
worker:

	pse_release_pis(pcdev);		/* kfree(pcdev->pi) */
	if (pcdev->irq)
		disable_irq(pcdev->irq);
	cancel_work_sync(&pcdev->ntf_work);

so the worker's pse_control_put() reaches __pse_control_release() and reads
psec->pcdev->pi[psec->id].admin_state_enabled after the array is gone. The
still-enabled IRQ can also queue more notification work before
cancel_work_sync(). Should disable_irq() and cancel_work_sync() move ahead
of pse_flush_pw_ds()/pse_release_pis()?

> +
> +static int phy_pse_notifier_event(struct notifier_block *nb,
> +				  unsigned long event, void *data)
> +{
> +	switch (event) {
> +	case PSE_REGISTERED:
> +		rtnl_lock();
> +		bus_for_each_dev(&mdio_bus_type, NULL, NULL,
> +				 phy_pse_attach_one);
> +		rtnl_unlock();
> +		return NOTIFY_OK;
> +	case PSE_UNREGISTERED:
> +		rtnl_lock();
> +		bus_for_each_dev(&mdio_bus_type, NULL, data,
> +				 phy_pse_detach_one);
> +		rtnl_unlock();
> +		return NOTIFY_OK;
> +	default:
> +		return NOTIFY_DONE;
> +	}
> +}

[Severity: High]

Does the detach walk actually close the use-after-free the changelog
describes, given that rtnl is dropped before the notifier returns?

pse_controller_unregister() unlinks the controller last:

	blocking_notifier_call_chain(&pse_controller_notifier,
				     PSE_UNREGISTERED, pcdev);
	pse_flush_pw_ds(pcdev);
	pse_release_pis(pcdev);		/* kfree(pcdev->pi) */
	...
	mutex_lock(&pse_list_mutex);
	list_del(&pcdev->list);

In the window after the notifier returns and before that list_del(), a phy
registering on another CPU runs phy_device_register() ->
phy_try_attach_pse() -> of_pse_control_get(), still finds the departing
pcdev on pse_controller_list, and creates a fresh psec on it. That path
touches the already freed pi array: of_pse_match_pi() reads
pcdev->pi[i].np, and pse_control_get_internal() reads pcdev->pi[index].rdev
and writes pcdev->pi[index].admin_state_enabled = ret.

Since the attach decision is taken under rtnl but the rest of the teardown
is not, this looks like a check-then-attach race against controller
teardown. Would unlinking pcdev from pse_controller_list before the
notifier fires, or publishing a "dying" state under the same lock the
attach uses, be needed here?

> @@ -1152,8 +1299,6 @@ EXPORT_SYMBOL(phy_device_register);
>  void phy_device_remove(struct phy_device *phydev)
>  {
>  	unregister_mii_timestamper(phydev->mii_ts);
> -	pse_control_put(phydev->psec);
> -
>  	device_del(&phydev->mdio.dev);
>  
>  	/* Assert the reset signal */

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260906153102.959217-1-github%40szelinsky.de

  reply	other threads:[~2026-09-09  6:33 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-06 15:30 [PATCH net-next v6 0/5] net: pse-pd: decouple controller lookup from MDIO probe Carlo Szelinsky
2026-09-06 15:30 ` [PATCH net-next v6 1/5] net: pse-pd: add notifier chain for controller lifecycle events Carlo Szelinsky
2026-09-09  6:33   ` netdev-bot+sashiko
2026-09-06 15:30 ` [PATCH net-next v6 2/5] net: pse-pd: fire lifecycle events on controller register/unregister Carlo Szelinsky
2026-09-09  6:33   ` netdev-bot+sashiko
2026-09-06 15:31 ` [PATCH net-next v6 3/5] net: phy: own phydev->psec via PSE notifier and remove fwnode_mdio hook Carlo Szelinsky
2026-09-09  6:33   ` netdev-bot+sashiko [this message]
2026-09-06 15:31 ` [PATCH net-next v6 4/5] net: phy: use a dedicated mutex instead of rtnl for PSE control attach Carlo Szelinsky
2026-09-09  6:33   ` netdev-bot+sashiko
2026-09-06 15:31 ` [PATCH net-next v6 5/5] net: phy: release phydev->psec from phy_device_remove() again Carlo Szelinsky
2026-09-09  6:33   ` netdev-bot+sashiko
2026-09-10  9:06 ` [PATCH net-next v6 0/5] net: pse-pd: decouple controller lookup from MDIO probe Paolo Abeni

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=178893559852.219967.17408171091451392990@kernel.org \
    --to=netdev-bot+sashiko@kernel.org \
    --cc=andrew+netdev@lunn.ch \
    --cc=corey@leavitt.info \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=github@szelinsky.de \
    --cc=hkallweit1@gmail.com \
    --cc=horms@kernel.org \
    --cc=jelonek.jonas@gmail.com \
    --cc=kory.maincent@bootlin.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@armlinux.org.uk \
    --cc=netdev@vger.kernel.org \
    --cc=o.rempel@pengutronix.de \
    --cc=olek2@wp.pl \
    --cc=pabeni@redhat.com \
    /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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®