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

From: Jacek Anaszewski
Date: Wed Jan 17 2018 - 16:14:14 EST


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