diff mbox series

leds: trigger: use RCU to protect the led_cdevs list

Message ID 20210915181601.99a68f5718be.I1a28b342d2d52cdeeeb81ecd6020c25cbf1dbfc0@changeid
State New
Headers show
Series leds: trigger: use RCU to protect the led_cdevs list | expand

Commit Message

Johannes Berg Sept. 15, 2021, 4:16 p.m. UTC
From: Johannes Berg <johannes.berg@intel.com>

Even with the previous commit 27af8e2c90fb
("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.)

Cc: stable@vger.kernel.org
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
 drivers/leds/led-triggers.c | 41 +++++++++++++++++++------------------
 include/linux/leds.h        |  2 +-
 2 files changed, 22 insertions(+), 21 deletions(-)

Comments

Pavel Machek Sept. 27, 2021, 2:16 p.m. UTC | #1
Hi!

> From: Johannes Berg <johannes.berg@intel.com>

> 

> Even with the previous commit 27af8e2c90fb

> ("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.)

> 

> Cc: stable@vger.kernel.org


I ... don't like idea of this going to stable.

Patch looks reasonable, but more eyes would be useful here. Is anyone
else willing to review it?

Realtime people hit related problem with locking, let me cc you.

Best regards,
							Pavel
							

> Signed-off-by: Johannes Berg <johannes.berg@intel.com>

> ---

>  drivers/leds/led-triggers.c | 41 +++++++++++++++++++------------------

>  include/linux/leds.h        |  2 +-

>  2 files changed, 22 insertions(+), 21 deletions(-)

> 

> diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c

> index 4e7b78a84149..072491d3e17b 100644

> --- a/drivers/leds/led-triggers.c

> +++ b/drivers/leds/led-triggers.c

> @@ -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;

> @@ -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)

> @@ -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)

> @@ -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);

> @@ -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);

> @@ -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);

>  

> @@ -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,

> diff --git a/include/linux/leds.h b/include/linux/leds.h

> index a0b730be40ad..ba4861ec73d3 100644

> --- a/include/linux/leds.h

> +++ b/include/linux/leds.h

> @@ -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 */

> -- 

> 2.31.1


-- 
http://www.livejournal.com/~pavelmachek
Johannes Berg Sept. 27, 2021, 2:20 p.m. UTC | #2
Hi Pavel,

> > 

> > Cc: stable@vger.kernel.org

> 

> I ... don't like idea of this going to stable.


OK, I guess that's fair. We've been running into the lockdep report for
a while - ever since we made the spinlock in iwlwifi no longer disable
IRQs all the time, which was meant to be a good thing ...

However, the scenario that could cause *real* deadlocks there is really
unlikely and requires four CPUs all doing the exactly right thing,
including a normally very rare *write lock* on the leddev_list_lock, so
I don't think in practice it'll be much of an issue.

johannes
diff mbox series

Patch

diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c
index 4e7b78a84149..072491d3e17b 100644
--- a/drivers/leds/led-triggers.c
+++ b/drivers/leds/led-triggers.c
@@ -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;
@@ -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)
@@ -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)
@@ -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);
@@ -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);
@@ -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);
 
@@ -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,
diff --git a/include/linux/leds.h b/include/linux/leds.h
index a0b730be40ad..ba4861ec73d3 100644
--- a/include/linux/leds.h
+++ b/include/linux/leds.h
@@ -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 */