Re: [PATCH] led: core: Fix race on software blink cancellation

From: Jacek Anaszewski
Date: Thu Feb 08 2018 - 15:57:08 EST


Any comments? I'd like to have some acks before applying
this patch.

Adding more people showing recently interest in the LED subsystem
related development.

Thanks,
Jacek Anaszewski

On 01/17/2018 10:12 PM, Jacek Anaszewski wrote:
> Commit d23a22a74fde ("leds: delay led_set_brightness if stopping soft-blink")
> made a modifications to the LED core allowing for led_set_brightness() to be
> called from hard-irq context when soft blink is being handled in soft-irq.
>
> Since that time LED core has undergone modifications related to addition of
> generic support for delegating brightness setting to a workqueue as well as
> subsequent fixes for blink setting use cases.
>
> After that the LED core code became hard to maintain and analyze, especially
> due to the imposed hard-irq context compatibility. It also turned out that
> in some cases a LED remained off after executing following sequence of commands:
>
> 1. echo timer > trigger
> 2. echo 0 > brightness
> 3. echo 100 > brightness
>
> The reason was LED_BLINK_DISABLE operation delegated to a set_brightness_work,
> triggered in 2., which was handled after 3. took effect.
>
> In order to serialize above operations and in the same time avoid
> code overcomplication the hard-irq context compatibility is being removed,
> which allows to use spin_lock_bh() for LED blink setting serialization.
>
>>From now, if in hard-irq context, users need to delegate led_set_brightness()
> to a workqueue in the place of use.
>
> Reported-by: Craig McQueen <craig.mcqueen@xxxxxxxxxxxxxxxxx>
> Signed-off-by: Jacek Anaszewski <jacek.anaszewski@xxxxxxxxx>
> ---
> Craig,
>
> It would be great if you could confirm if this fixes your use case.
>
> drivers/leds/led-core.c | 79 +++++++++++++++++++-------------
> drivers/leds/trigger/ledtrig-activity.c | 3 --
> drivers/leds/trigger/ledtrig-heartbeat.c | 3 --
> include/linux/leds.h | 5 +-
> 4 files changed, 49 insertions(+), 41 deletions(-)
>
> diff --git a/drivers/leds/led-core.c b/drivers/leds/led-core.c
> index ede4fa0..25d711b 100644
> --- a/drivers/leds/led-core.c
> +++ b/drivers/leds/led-core.c
> @@ -22,6 +22,9 @@
> DECLARE_RWSEM(leds_list_lock);
> EXPORT_SYMBOL_GPL(leds_list_lock);
>
> +DEFINE_SPINLOCK(leds_soft_blink_lock);
> +EXPORT_SYMBOL_GPL(leds_soft_blink_lock);
> +
> LIST_HEAD(leds_list);
> EXPORT_SYMBOL_GPL(leds_list);
>
> @@ -51,26 +54,31 @@ static void led_timer_function(struct timer_list *t)
> unsigned long brightness;
> unsigned long delay;
>
> + spin_lock_bh(&leds_soft_blink_lock);
> +
> + /*
> + * Check if soft blinking wasn't disabled via led_set_brightness()
> + * in the meantime.
> + */
> + if (!test_bit(LED_BLINK_SW, &led_cdev->work_flags))
> + goto unlock;
> +
> if (!led_cdev->blink_delay_on || !led_cdev->blink_delay_off) {
> led_set_brightness_nosleep(led_cdev, LED_OFF);
> clear_bit(LED_BLINK_SW, &led_cdev->work_flags);
> - return;
> + goto unlock;
> }
>
> if (test_and_clear_bit(LED_BLINK_ONESHOT_STOP,
> &led_cdev->work_flags)) {
> clear_bit(LED_BLINK_SW, &led_cdev->work_flags);
> - return;
> + goto unlock;
> }
>
> brightness = led_get_brightness(led_cdev);
> if (!brightness) {
> /* Time to switch the LED on. */
> - if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE,
> - &led_cdev->work_flags))
> - brightness = led_cdev->new_blink_brightness;
> - else
> - brightness = led_cdev->blink_brightness;
> + brightness = led_cdev->blink_brightness;
> delay = led_cdev->blink_delay_on;
> } else {
> /* Store the current brightness value to be able
> @@ -100,6 +108,9 @@ static void led_timer_function(struct timer_list *t)
> }
>
> mod_timer(&led_cdev->blink_timer, jiffies + msecs_to_jiffies(delay));
> +
> +unlock:
> + spin_unlock_bh(&leds_soft_blink_lock);
> }
>
> static void set_brightness_delayed(struct work_struct *ws)
> @@ -108,11 +119,6 @@ static void set_brightness_delayed(struct work_struct *ws)
> container_of(ws, struct led_classdev, set_brightness_work);
> int ret = 0;
>
> - if (test_and_clear_bit(LED_BLINK_DISABLE, &led_cdev->work_flags)) {
> - led_cdev->delayed_set_value = LED_OFF;
> - led_stop_software_blink(led_cdev);
> - }
> -
> ret = __led_set_brightness(led_cdev, led_cdev->delayed_set_value);
> if (ret == -ENOTSUPP)
> ret = __led_set_brightness_blocking(led_cdev,
> @@ -131,6 +137,8 @@ static void led_set_software_blink(struct led_classdev *led_cdev,
> {
> int current_brightness;
>
> + spin_lock_bh(&leds_soft_blink_lock);
> +
> current_brightness = led_get_brightness(led_cdev);
> if (current_brightness)
> led_cdev->blink_brightness = current_brightness;
> @@ -143,18 +151,21 @@ static void led_set_software_blink(struct led_classdev *led_cdev,
> /* never on - just set to off */
> if (!delay_on) {
> led_set_brightness_nosleep(led_cdev, LED_OFF);
> - return;
> + goto unlock;
> }
>
> /* never off - just set to brightness */
> if (!delay_off) {
> led_set_brightness_nosleep(led_cdev,
> led_cdev->blink_brightness);
> - return;
> + goto unlock;
> }
>
> set_bit(LED_BLINK_SW, &led_cdev->work_flags);
> mod_timer(&led_cdev->blink_timer, jiffies + 1);
> +
> +unlock:
> + spin_unlock_bh(&leds_soft_blink_lock);
> }
>
>
> @@ -217,40 +228,46 @@ void led_blink_set_oneshot(struct led_classdev *led_cdev,
> }
> EXPORT_SYMBOL_GPL(led_blink_set_oneshot);
>
> -void led_stop_software_blink(struct led_classdev *led_cdev)
> +void led_stop_software_blink_nolock(struct led_classdev *led_cdev)
> {
> - del_timer_sync(&led_cdev->blink_timer);
> led_cdev->blink_delay_on = 0;
> led_cdev->blink_delay_off = 0;
> clear_bit(LED_BLINK_SW, &led_cdev->work_flags);
> }
> +
> +void led_stop_software_blink(struct led_classdev *led_cdev)
> +{
> + spin_lock_bh(&leds_soft_blink_lock);
> + led_stop_software_blink_nolock(led_cdev);
> + spin_unlock_bh(&leds_soft_blink_lock);
> +}
> EXPORT_SYMBOL_GPL(led_stop_software_blink);
>
> void led_set_brightness(struct led_classdev *led_cdev,
> enum led_brightness brightness)
> {
> - /*
> - * If software blink is active, delay brightness setting
> - * until the next timer tick.
> - */
> + if (in_irq()) {
> + dev_err(led_cdev->dev,
> + "Cannot set LED brightness from hard irq context!");
> + WARN_ON(1);
> + return;
> + }
> +
> + spin_lock_bh(&leds_soft_blink_lock);
> +
> if (test_bit(LED_BLINK_SW, &led_cdev->work_flags)) {
> - /*
> - * If we need to disable soft blinking delegate this to the
> - * work queue task to avoid problems in case we are called
> - * from hard irq context.
> - */
> if (brightness == LED_OFF) {
> - set_bit(LED_BLINK_DISABLE, &led_cdev->work_flags);
> - schedule_work(&led_cdev->set_brightness_work);
> + led_stop_software_blink_nolock(led_cdev);
> } else {
> - set_bit(LED_BLINK_BRIGHTNESS_CHANGE,
> - &led_cdev->work_flags);
> - led_cdev->new_blink_brightness = brightness;
> + led_cdev->blink_brightness = brightness;
> + goto unlock;
> }
> - return;
> }
>
> led_set_brightness_nosleep(led_cdev, brightness);
> +
> +unlock:
> + spin_unlock_bh(&leds_soft_blink_lock);
> }
> EXPORT_SYMBOL_GPL(led_set_brightness);
>
> diff --git a/drivers/leds/trigger/ledtrig-activity.c b/drivers/leds/trigger/ledtrig-activity.c
> index 5081894..6f62da7 100644
> --- a/drivers/leds/trigger/ledtrig-activity.c
> +++ b/drivers/leds/trigger/ledtrig-activity.c
> @@ -48,9 +48,6 @@ static void led_activity_function(struct timer_list *t)
> int cpus;
> int i;
>
> - if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE, &led_cdev->work_flags))
> - led_cdev->blink_brightness = led_cdev->new_blink_brightness;
> -
> if (unlikely(panic_detected)) {
> /* full brightness in case of panic */
> led_set_brightness_nosleep(led_cdev, led_cdev->blink_brightness);
> diff --git a/drivers/leds/trigger/ledtrig-heartbeat.c b/drivers/leds/trigger/ledtrig-heartbeat.c
> index f0896de..1453352 100644
> --- a/drivers/leds/trigger/ledtrig-heartbeat.c
> +++ b/drivers/leds/trigger/ledtrig-heartbeat.c
> @@ -47,9 +47,6 @@ static void led_heartbeat_function(struct timer_list *t)
> return;
> }
>
> - if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE, &led_cdev->work_flags))
> - led_cdev->blink_brightness = led_cdev->new_blink_brightness;
> -
> /* acts like an actual heart beat -- ie thump-thump-pause... */
> switch (heartbeat_data->phase) {
> case 0:
> diff --git a/include/linux/leds.h b/include/linux/leds.h
> index 5579c64..e520503 100644
> --- a/include/linux/leds.h
> +++ b/include/linux/leds.h
> @@ -51,15 +51,13 @@ struct led_classdev {
> #define LED_BRIGHT_HW_CHANGED BIT(21)
> #define LED_RETAIN_AT_SHUTDOWN BIT(22)
>
> - /* set_brightness_work / blink_timer flags, atomic, private. */
> + /* blink_timer flags, atomic, private. */
> unsigned long work_flags;
>
> #define LED_BLINK_SW 0
> #define LED_BLINK_ONESHOT 1
> #define LED_BLINK_ONESHOT_STOP 2
> #define LED_BLINK_INVERT 3
> -#define LED_BLINK_BRIGHTNESS_CHANGE 4
> -#define LED_BLINK_DISABLE 5
>
> /* Set LED brightness level
> * Must not sleep. Use brightness_set_blocking for drivers
> @@ -97,7 +95,6 @@ struct led_classdev {
> unsigned long blink_delay_on, blink_delay_off;
> struct timer_list blink_timer;
> int blink_brightness;
> - int new_blink_brightness;
> void (*flash_resume)(struct led_classdev *led_cdev);
>
> struct work_struct set_brightness_work;
>