Skip to content

Commit

Permalink
leds: trigger: use RCU to protect the led_cdevs list
Browse files Browse the repository at this point in the history
Even with the previous commit 27af8e2
("leds: trigger: fix potential deadlock with libata")
to this file, we still get lockdep unhappy, and Boqun
explained the report here:
https://lore.kernel.org/r/YNA+d1X4UkoQ7g8a@boqun-archlinux

Effectively, this means that the read_lock_irqsave() isn't
enough here because another CPU might be trying to do a
write lock, and thus block the readers.

This is all pretty messy, but it doesn't seem right that
the LEDs framework imposes some locking requirements on
users, in particular we'd have to make the spinlock in the
iwlwifi driver always disable IRQs, even if we don't need
that for any other reason, just to avoid this deadlock.

Since writes to the led_cdevs list are rare (and are done
by userspace), just switch the list to RCU. This costs a
synchronize_rcu() at removal time so we can ensure things
are correct, but that seems like a small price to pay for
getting lock-free iterations and no deadlocks (nor any
locking requirements imposed on users.)

Signed-off-by: Johannes Berg <johannes.berg@intel.com>
Signed-off-by: Pavel Machek <pavel@ucw.cz>
  • Loading branch information
jmberg-intel authored and pavelmachek committed Sep 27, 2021
1 parent 811b544 commit 2a5a8fa
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 21 deletions.
41 changes: 21 additions & 20 deletions drivers/leds/led-triggers.c
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ EXPORT_SYMBOL_GPL(led_trigger_read);
/* Caller must ensure led_cdev->trigger_lock held */
int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
{
unsigned long flags;
char *event = NULL;
char *envp[2];
const char *name;
Expand All @@ -171,10 +170,13 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)

/* Remove any existing trigger */
if (led_cdev->trigger) {
write_lock_irqsave(&led_cdev->trigger->leddev_list_lock, flags);
list_del(&led_cdev->trig_list);
write_unlock_irqrestore(&led_cdev->trigger->leddev_list_lock,
flags);
spin_lock(&led_cdev->trigger->leddev_list_lock);
list_del_rcu(&led_cdev->trig_list);
spin_unlock(&led_cdev->trigger->leddev_list_lock);

/* ensure it's no longer visible on the led_cdevs list */
synchronize_rcu();

cancel_work_sync(&led_cdev->set_brightness_work);
led_stop_software_blink(led_cdev);
if (led_cdev->trigger->deactivate)
Expand All @@ -186,9 +188,9 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
led_set_brightness(led_cdev, LED_OFF);
}
if (trig) {
write_lock_irqsave(&trig->leddev_list_lock, flags);
list_add_tail(&led_cdev->trig_list, &trig->led_cdevs);
write_unlock_irqrestore(&trig->leddev_list_lock, flags);
spin_lock(&trig->leddev_list_lock);
list_add_tail_rcu(&led_cdev->trig_list, &trig->led_cdevs);
spin_unlock(&trig->leddev_list_lock);
led_cdev->trigger = trig;

if (trig->activate)
Expand Down Expand Up @@ -223,9 +225,10 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
trig->deactivate(led_cdev);
err_activate:

write_lock_irqsave(&led_cdev->trigger->leddev_list_lock, flags);
list_del(&led_cdev->trig_list);
write_unlock_irqrestore(&led_cdev->trigger->leddev_list_lock, flags);
spin_lock(&led_cdev->trigger->leddev_list_lock);
list_del_rcu(&led_cdev->trig_list);
spin_unlock(&led_cdev->trigger->leddev_list_lock);
synchronize_rcu();
led_cdev->trigger = NULL;
led_cdev->trigger_data = NULL;
led_set_brightness(led_cdev, LED_OFF);
Expand Down Expand Up @@ -285,7 +288,7 @@ int led_trigger_register(struct led_trigger *trig)
struct led_classdev *led_cdev;
struct led_trigger *_trig;

rwlock_init(&trig->leddev_list_lock);
spin_lock_init(&trig->leddev_list_lock);
INIT_LIST_HEAD(&trig->led_cdevs);

down_write(&triggers_list_lock);
Expand Down Expand Up @@ -378,15 +381,14 @@ void led_trigger_event(struct led_trigger *trig,
enum led_brightness brightness)
{
struct led_classdev *led_cdev;
unsigned long flags;

if (!trig)
return;

read_lock_irqsave(&trig->leddev_list_lock, flags);
list_for_each_entry(led_cdev, &trig->led_cdevs, trig_list)
rcu_read_lock();
list_for_each_entry_rcu(led_cdev, &trig->led_cdevs, trig_list)
led_set_brightness(led_cdev, brightness);
read_unlock_irqrestore(&trig->leddev_list_lock, flags);
rcu_read_unlock();
}
EXPORT_SYMBOL_GPL(led_trigger_event);

Expand All @@ -397,20 +399,19 @@ static void led_trigger_blink_setup(struct led_trigger *trig,
int invert)
{
struct led_classdev *led_cdev;
unsigned long flags;

if (!trig)
return;

read_lock_irqsave(&trig->leddev_list_lock, flags);
list_for_each_entry(led_cdev, &trig->led_cdevs, trig_list) {
rcu_read_lock();
list_for_each_entry_rcu(led_cdev, &trig->led_cdevs, trig_list) {
if (oneshot)
led_blink_set_oneshot(led_cdev, delay_on, delay_off,
invert);
else
led_blink_set(led_cdev, delay_on, delay_off);
}
read_unlock_irqrestore(&trig->leddev_list_lock, flags);
rcu_read_unlock();
}

void led_trigger_blink(struct led_trigger *trig,
Expand Down
2 changes: 1 addition & 1 deletion include/linux/leds.h
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ struct led_trigger {
struct led_hw_trigger_type *trigger_type;

/* LEDs under control by this trigger (for simple triggers) */
rwlock_t leddev_list_lock;
spinlock_t leddev_list_lock;
struct list_head led_cdevs;

/* Link to next registered trigger */
Expand Down

0 comments on commit 2a5a8fa

Please sign in to comment.