The notification worker holds a reference on the pse_control, but not on the PHY that psec->attached_phydev points to. If the PHY is removed while the worker runs, the pse_control survives and the worker reads the freed PHY. The same happens without a race when two PHYs share one PI: attached_phydev keeps pointing at the first PHY after it is removed. pse_control_put() unlinks and frees a pse_control under pse_list_mutex, and phy_device_remove() calls it before the PHY is freed. So instead of taking a reference, let the worker look the pse_control up and use attached_phydev under pse_list_mutex. This nests pse_list_mutex inside rtnl; nothing under pse_list_mutex takes rtnl. For a PI shared by two PHYs there is no single netdev to report to, so clear attached_phydev when a second PHY takes the pse_control, and warn. Events for that PI are then no longer reported, even after one PHY goes away. Failing the second PHY instead would fail its whole MDIO bus. Triggered in QEMU (arm64, KASAN) with a test-only PSE controller: removing a PHY while the worker waits for rtnl, and removing one of two PHYs on a shared PI, both give BUG: KASAN: slab-use-after-free in pse_send_ntf_worker+0x2b0/0x2bc With this patch both are gone. Fixes: fc0e6db30941 ("net: pse-pd: Add support for reporting events") Reviewed-by: Kory Maincent Acked-by: Oleksij Rempel Signed-off-by: Carlo Szelinsky --- drivers/net/pse-pd/pse_core.c | 42 ++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/drivers/net/pse-pd/pse_core.c b/drivers/net/pse-pd/pse_core.c index eab7e25f2039..44bc6d557965 100644 --- a/drivers/net/pse-pd/pse_core.c +++ b/drivers/net/pse-pd/pse_core.c @@ -31,7 +31,9 @@ static DEFINE_MUTEX(pse_pw_d_mutex); * @list: list entry for the pcdev's PSE controller list * @id: ID of the PSE line in the PSE controller device * @refcnt: Number of gets of this pse_control - * @attached_phydev: PHY device pointer attached by the PSE control + * @attached_phydev: PHY device pointer attached by the PSE control, or NULL + * when several PHYs share the PI. Read it only under + * pse_list_mutex. */ struct pse_control { struct pse_controller_dev *pcdev; @@ -238,24 +240,22 @@ static int of_load_pse_pis(struct pse_controller_dev *pcdev) * @pcdev: a pointer to the PSE * @id: index of the PSE control * - * Return: pse_control pointer or NULL. The device returned has had a - * reference added and the pointer is safe until the user calls - * pse_control_put() to indicate they have finished with it. + * Must be called with pse_list_mutex held. No reference is taken: the + * pointer is only valid until pse_list_mutex is released. + * + * Return: pse_control pointer or NULL. */ static struct pse_control * pse_control_find_by_id(struct pse_controller_dev *pcdev, int id) { struct pse_control *psec; - mutex_lock(&pse_list_mutex); + lockdep_assert_held(&pse_list_mutex); + list_for_each_entry(psec, &pcdev->pse_control_head, list) { - if (psec->id == id) { - kref_get(&psec->refcnt); - mutex_unlock(&pse_list_mutex); + if (psec->id == id) return psec; - } } - mutex_unlock(&pse_list_mutex); return NULL; } @@ -263,11 +263,14 @@ pse_control_find_by_id(struct pse_controller_dev *pcdev, int id) * pse_control_get_netdev - Return netdev associated to a PSE control * @psec: PSE control pointer * - * Return: netdev pointer or NULL + * Must be called with rtnl and pse_list_mutex held. + * + * Return: netdev pointer, or NULL if no single PHY is attached */ static struct net_device *pse_control_get_netdev(struct pse_control *psec) { ASSERT_RTNL(); + lockdep_assert_held(&pse_list_mutex); if (!psec || !psec->attached_phydev) return NULL; @@ -1015,13 +1018,18 @@ static void pse_send_ntf_worker(struct work_struct *work) struct net_device *netdev; struct pse_control *psec; - psec = pse_control_find_by_id(pcdev, ntf.id); + /* pse_control_put() frees the pse_control under + * pse_list_mutex before its PHY is freed, so the mutex + * keeps attached_phydev valid. + */ rtnl_lock(); + mutex_lock(&pse_list_mutex); + psec = pse_control_find_by_id(pcdev, ntf.id); netdev = pse_control_get_netdev(psec); if (netdev) ethnl_pse_send_ntf(netdev, ntf.notifs); + mutex_unlock(&pse_list_mutex); rtnl_unlock(); - pse_control_put(psec); } } @@ -1412,6 +1420,14 @@ pse_control_get_internal(struct pse_controller_dev *pcdev, unsigned int index, list_for_each_entry(psec, &pcdev->pse_control_head, list) { if (psec->id == index) { + if (psec->attached_phydev && + psec->attached_phydev != phydev) { + dev_warn(pcdev->dev, + "PI %u shared by several PHYs, not reporting its events\n", + index); + psec->attached_phydev = NULL; + } + kref_get(&psec->refcnt); return psec; } -- 2.43.0