1
0
Fork 0
mirror of synced 2025-03-06 20:59:54 +01:00
linux/drivers/net/ethernet/intel/igc/igc_leds.c
Lukas Wunner c04d1b9ecc igc: Fix LED-related deadlock on driver unbind
Roman reports a deadlock on unplug of a Thunderbolt docking station
containing an Intel I225 Ethernet adapter.

The root cause is that led_classdev's for LEDs on the adapter are
registered such that they're device-managed by the netdev.  That
results in recursive acquisition of the rtnl_lock() mutex on unplug:

When the driver calls unregister_netdev(), it acquires rtnl_lock(),
then frees the device-managed resources.  Upon unregistering the LEDs,
netdev_trig_deactivate() invokes unregister_netdevice_notifier(),
which tries to acquire rtnl_lock() again.

Avoid by using non-device-managed LED registration.

Stack trace for posterity:

  schedule+0x6e/0xf0
  schedule_preempt_disabled+0x15/0x20
  __mutex_lock+0x2a0/0x750
  unregister_netdevice_notifier+0x40/0x150
  netdev_trig_deactivate+0x1f/0x60 [ledtrig_netdev]
  led_trigger_set+0x102/0x330
  led_classdev_unregister+0x4b/0x110
  release_nodes+0x3d/0xb0
  devres_release_all+0x8b/0xc0
  device_del+0x34f/0x3c0
  unregister_netdevice_many_notify+0x80b/0xaf0
  unregister_netdev+0x7c/0xd0
  igc_remove+0xd8/0x1e0 [igc]
  pci_device_remove+0x3f/0xb0

Fixes: ea578703b0 ("igc: Add support for LEDs on i225/i226")
Reported-by: Roman Lozko <lozko.roma@gmail.com>
Closes: https://lore.kernel.org/r/CAEhC_B=ksywxCG_+aQqXUrGEgKq+4mqnSV8EBHOKbC3-Obj9+Q@mail.gmail.com/
Reported-by: "Marek Marczykowski-Górecki" <marmarek@invisiblethingslab.com>
Closes: https://lore.kernel.org/r/ZhRD3cOtz5i-61PB@mail-itl/
Signed-off-by: Kurt Kanzenbach <kurt@linutronix.de>
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Cc: Heiner Kallweit <hkallweit1@gmail.com>
Reviewed-by: Simon Horman <horms@kernel.org>
Reviewed-by: Kurt Kanzenbach <kurt@linutronix.de>
Tested-by: Kurt Kanzenbach <kurt@linutronix.de> # Intel i225
Tested-by: Naama Meir <naamax.meir@linux.intel.com>
Signed-off-by: Tony Nguyen <anthony.l.nguyen@intel.com>
Link: https://lore.kernel.org/r/20240422204503.225448-1-anthony.l.nguyen@intel.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
2024-04-24 20:09:30 -07:00

302 lines
7.6 KiB
C

// SPDX-License-Identifier: GPL-2.0
/* Copyright (C) 2024 Linutronix GmbH */
#include <linux/bits.h>
#include <linux/leds.h>
#include <linux/netdevice.h>
#include <linux/pm_runtime.h>
#include <uapi/linux/uleds.h>
#include "igc.h"
#define IGC_NUM_LEDS 3
#define IGC_LEDCTL_LED0_MODE_SHIFT 0
#define IGC_LEDCTL_LED0_MODE_MASK GENMASK(3, 0)
#define IGC_LEDCTL_LED0_BLINK BIT(7)
#define IGC_LEDCTL_LED1_MODE_SHIFT 8
#define IGC_LEDCTL_LED1_MODE_MASK GENMASK(11, 8)
#define IGC_LEDCTL_LED1_BLINK BIT(15)
#define IGC_LEDCTL_LED2_MODE_SHIFT 16
#define IGC_LEDCTL_LED2_MODE_MASK GENMASK(19, 16)
#define IGC_LEDCTL_LED2_BLINK BIT(23)
#define IGC_LEDCTL_MODE_ON 0x00
#define IGC_LEDCTL_MODE_OFF 0x01
#define IGC_LEDCTL_MODE_LINK_10 0x05
#define IGC_LEDCTL_MODE_LINK_100 0x06
#define IGC_LEDCTL_MODE_LINK_1000 0x07
#define IGC_LEDCTL_MODE_LINK_2500 0x08
#define IGC_LEDCTL_MODE_ACTIVITY 0x0b
#define IGC_SUPPORTED_MODES \
(BIT(TRIGGER_NETDEV_LINK_2500) | BIT(TRIGGER_NETDEV_LINK_1000) | \
BIT(TRIGGER_NETDEV_LINK_100) | BIT(TRIGGER_NETDEV_LINK_10) | \
BIT(TRIGGER_NETDEV_RX) | BIT(TRIGGER_NETDEV_TX))
#define IGC_ACTIVITY_MODES \
(BIT(TRIGGER_NETDEV_RX) | BIT(TRIGGER_NETDEV_TX))
struct igc_led_classdev {
struct net_device *netdev;
struct led_classdev led;
int index;
};
#define lcdev_to_igc_ldev(lcdev) \
container_of(lcdev, struct igc_led_classdev, led)
static void igc_led_select(struct igc_adapter *adapter, int led,
u32 *mask, u32 *shift, u32 *blink)
{
switch (led) {
case 0:
*mask = IGC_LEDCTL_LED0_MODE_MASK;
*shift = IGC_LEDCTL_LED0_MODE_SHIFT;
*blink = IGC_LEDCTL_LED0_BLINK;
break;
case 1:
*mask = IGC_LEDCTL_LED1_MODE_MASK;
*shift = IGC_LEDCTL_LED1_MODE_SHIFT;
*blink = IGC_LEDCTL_LED1_BLINK;
break;
case 2:
*mask = IGC_LEDCTL_LED2_MODE_MASK;
*shift = IGC_LEDCTL_LED2_MODE_SHIFT;
*blink = IGC_LEDCTL_LED2_BLINK;
break;
default:
*mask = *shift = *blink = 0;
netdev_err(adapter->netdev, "Unknown LED %d selected!\n", led);
}
}
static void igc_led_set(struct igc_adapter *adapter, int led, u32 mode,
bool blink)
{
u32 shift, mask, blink_bit, ledctl;
struct igc_hw *hw = &adapter->hw;
igc_led_select(adapter, led, &mask, &shift, &blink_bit);
pm_runtime_get_sync(&adapter->pdev->dev);
mutex_lock(&adapter->led_mutex);
/* Set mode */
ledctl = rd32(IGC_LEDCTL);
ledctl &= ~mask;
ledctl |= mode << shift;
/* Configure blinking */
if (blink)
ledctl |= blink_bit;
else
ledctl &= ~blink_bit;
wr32(IGC_LEDCTL, ledctl);
mutex_unlock(&adapter->led_mutex);
pm_runtime_put(&adapter->pdev->dev);
}
static u32 igc_led_get(struct igc_adapter *adapter, int led)
{
u32 shift, mask, blink_bit, ledctl;
struct igc_hw *hw = &adapter->hw;
igc_led_select(adapter, led, &mask, &shift, &blink_bit);
pm_runtime_get_sync(&adapter->pdev->dev);
mutex_lock(&adapter->led_mutex);
ledctl = rd32(IGC_LEDCTL);
mutex_unlock(&adapter->led_mutex);
pm_runtime_put(&adapter->pdev->dev);
return (ledctl & mask) >> shift;
}
static int igc_led_brightness_set_blocking(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);
struct igc_adapter *adapter = netdev_priv(ldev->netdev);
u32 mode;
if (brightness)
mode = IGC_LEDCTL_MODE_ON;
else
mode = IGC_LEDCTL_MODE_OFF;
netdev_dbg(adapter->netdev, "Set brightness for LED %d to mode %u!\n",
ldev->index, mode);
igc_led_set(adapter, ldev->index, mode, false);
return 0;
}
static int igc_led_hw_control_is_supported(struct led_classdev *led_cdev,
unsigned long flags)
{
if (flags & ~IGC_SUPPORTED_MODES)
return -EOPNOTSUPP;
/* If Tx and Rx selected, activity can be offloaded unless some other
* mode is selected as well.
*/
if ((flags & BIT(TRIGGER_NETDEV_TX)) &&
(flags & BIT(TRIGGER_NETDEV_RX)) &&
!(flags & ~IGC_ACTIVITY_MODES))
return 0;
/* Single Rx or Tx activity is not supported. */
if (flags & IGC_ACTIVITY_MODES)
return -EOPNOTSUPP;
/* Only one mode can be active at a given time. */
if (flags & (flags - 1))
return -EOPNOTSUPP;
return 0;
}
static int igc_led_hw_control_set(struct led_classdev *led_cdev,
unsigned long flags)
{
struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);
struct igc_adapter *adapter = netdev_priv(ldev->netdev);
u32 mode = IGC_LEDCTL_MODE_OFF;
bool blink = false;
if (flags & BIT(TRIGGER_NETDEV_LINK_10))
mode = IGC_LEDCTL_MODE_LINK_10;
if (flags & BIT(TRIGGER_NETDEV_LINK_100))
mode = IGC_LEDCTL_MODE_LINK_100;
if (flags & BIT(TRIGGER_NETDEV_LINK_1000))
mode = IGC_LEDCTL_MODE_LINK_1000;
if (flags & BIT(TRIGGER_NETDEV_LINK_2500))
mode = IGC_LEDCTL_MODE_LINK_2500;
if ((flags & BIT(TRIGGER_NETDEV_TX)) &&
(flags & BIT(TRIGGER_NETDEV_RX)))
mode = IGC_LEDCTL_MODE_ACTIVITY;
netdev_dbg(adapter->netdev, "Set HW control for LED %d to mode %u!\n",
ldev->index, mode);
/* blink is recommended for activity */
if (mode == IGC_LEDCTL_MODE_ACTIVITY)
blink = true;
igc_led_set(adapter, ldev->index, mode, blink);
return 0;
}
static int igc_led_hw_control_get(struct led_classdev *led_cdev,
unsigned long *flags)
{
struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);
struct igc_adapter *adapter = netdev_priv(ldev->netdev);
u32 mode;
mode = igc_led_get(adapter, ldev->index);
switch (mode) {
case IGC_LEDCTL_MODE_ACTIVITY:
*flags = BIT(TRIGGER_NETDEV_TX) | BIT(TRIGGER_NETDEV_RX);
break;
case IGC_LEDCTL_MODE_LINK_10:
*flags = BIT(TRIGGER_NETDEV_LINK_10);
break;
case IGC_LEDCTL_MODE_LINK_100:
*flags = BIT(TRIGGER_NETDEV_LINK_100);
break;
case IGC_LEDCTL_MODE_LINK_1000:
*flags = BIT(TRIGGER_NETDEV_LINK_1000);
break;
case IGC_LEDCTL_MODE_LINK_2500:
*flags = BIT(TRIGGER_NETDEV_LINK_2500);
break;
}
return 0;
}
static struct device *igc_led_hw_control_get_device(struct led_classdev *led_cdev)
{
struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);
return &ldev->netdev->dev;
}
static void igc_led_get_name(struct igc_adapter *adapter, int index, char *buf,
size_t buf_len)
{
snprintf(buf, buf_len, "igc-%x%x-led%d",
pci_domain_nr(adapter->pdev->bus),
pci_dev_id(adapter->pdev), index);
}
static int igc_setup_ldev(struct igc_led_classdev *ldev,
struct net_device *netdev, int index)
{
struct igc_adapter *adapter = netdev_priv(netdev);
struct led_classdev *led_cdev = &ldev->led;
char led_name[LED_MAX_NAME_SIZE];
ldev->netdev = netdev;
ldev->index = index;
igc_led_get_name(adapter, index, led_name, LED_MAX_NAME_SIZE);
led_cdev->name = led_name;
led_cdev->flags |= LED_RETAIN_AT_SHUTDOWN;
led_cdev->max_brightness = 1;
led_cdev->brightness_set_blocking = igc_led_brightness_set_blocking;
led_cdev->hw_control_trigger = "netdev";
led_cdev->hw_control_is_supported = igc_led_hw_control_is_supported;
led_cdev->hw_control_set = igc_led_hw_control_set;
led_cdev->hw_control_get = igc_led_hw_control_get;
led_cdev->hw_control_get_device = igc_led_hw_control_get_device;
return led_classdev_register(&netdev->dev, led_cdev);
}
int igc_led_setup(struct igc_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
struct igc_led_classdev *leds;
int i, err;
mutex_init(&adapter->led_mutex);
leds = kcalloc(IGC_NUM_LEDS, sizeof(*leds), GFP_KERNEL);
if (!leds)
return -ENOMEM;
for (i = 0; i < IGC_NUM_LEDS; i++) {
err = igc_setup_ldev(leds + i, netdev, i);
if (err)
goto err;
}
adapter->leds = leds;
return 0;
err:
for (i--; i >= 0; i--)
led_classdev_unregister(&((leds + i)->led));
kfree(leds);
return err;
}
void igc_led_free(struct igc_adapter *adapter)
{
struct igc_led_classdev *leds = adapter->leds;
int i;
for (i = 0; i < IGC_NUM_LEDS; i++)
led_classdev_unregister(&((leds + i)->led));
kfree(leds);
}