Skip to content

Commit 2c70953

Browse files
repklag-linaro
authored andcommitted
leds: Fix LED_OFF brightness race
While commit fa15d8c ("leds: Fix set_brightness_delayed() race") successfully forces led_set_brightness() to be called with LED_OFF at least once when switching from blinking to LED on state so that hw-blinking can be disabled, another race remains. Indeed in led_set_brightness(LED_OFF) followed by led_set_brightness(any) scenario the following CPU scheduling can happen: CPU0 CPU1 ---- ---- set_brightness_delayed() { test_and_clear_bit(BRIGHTNESS_OFF) led_set_brightness(LED_OFF) { set_bit(BRIGHTNESS_OFF) queue_work() } led_set_brightness(any) { set_bit(BRIGHTNESS) queue_work() //already queued } test_and_clear_bit(BRIGHTNESS) /* LED set with brightness any */ } /* From previous CPU1 queue_work() */ set_brightness_delayed() { test_and_clear_bit(BRIGHTNESS_OFF) /* LED turned off */ test_and_clear_bit(BRIGHTNESS) /* Clear from previous run, LED remains off */ In that case the led_set_brightness(LED_OFF)/led_set_brightness(any) sequence will be effectively executed in reverse order and LED will remain off. With the introduction of commit 32360bf ("leds: Introduce ordered workqueue for LEDs events instead of system_wq") the race is easier to trigger as sysfs brightness configuration does not wait for set_brightness_delayed() work to finish (flush_work() removal). Use delayed_set_value to optionnally re-configure brightness after a LED_OFF. That way a LED state could be configured more that once but final state will always be as expected. Ensure that delayed_set_value modification is seen before set_bit() using smp_mb__before_atomic(). Fixes: fa15d8c ("leds: Fix set_brightness_delayed() race") Signed-off-by: Remi Pommarel <[email protected]> Reviewed-by: Hans de Goede <[email protected]> Link: https://lore.kernel.org/r/19c81177059dab7b656c42063958011a8e4d1a66.1740050412.git.repk@triplefau.lt Signed-off-by: Lee Jones <[email protected]>
1 parent 012825d commit 2c70953

File tree

1 file changed

+18
-4
lines changed

1 file changed

+18
-4
lines changed

drivers/leds/led-core.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,19 @@ static void set_brightness_delayed(struct work_struct *ws)
159159
* before this work item runs once. To make sure this works properly
160160
* handle LED_SET_BRIGHTNESS_OFF first.
161161
*/
162-
if (test_and_clear_bit(LED_SET_BRIGHTNESS_OFF, &led_cdev->work_flags))
162+
if (test_and_clear_bit(LED_SET_BRIGHTNESS_OFF, &led_cdev->work_flags)) {
163163
set_brightness_delayed_set_brightness(led_cdev, LED_OFF);
164+
/*
165+
* The consecutives led_set_brightness(LED_OFF),
166+
* led_set_brightness(LED_FULL) could have been executed out of
167+
* order (LED_FULL first), if the work_flags has been set
168+
* between LED_SET_BRIGHTNESS_OFF and LED_SET_BRIGHTNESS of this
169+
* work. To avoid ending with the LED turned off, turn the LED
170+
* on again.
171+
*/
172+
if (led_cdev->delayed_set_value != LED_OFF)
173+
set_bit(LED_SET_BRIGHTNESS, &led_cdev->work_flags);
174+
}
164175

165176
if (test_and_clear_bit(LED_SET_BRIGHTNESS, &led_cdev->work_flags))
166177
set_brightness_delayed_set_brightness(led_cdev, led_cdev->delayed_set_value);
@@ -331,10 +342,13 @@ void led_set_brightness_nopm(struct led_classdev *led_cdev, unsigned int value)
331342
* change is done immediately afterwards (before the work runs),
332343
* it uses a separate work_flag.
333344
*/
334-
if (value) {
335-
led_cdev->delayed_set_value = value;
345+
led_cdev->delayed_set_value = value;
346+
/* Ensure delayed_set_value is seen before work_flags modification */
347+
smp_mb__before_atomic();
348+
349+
if (value)
336350
set_bit(LED_SET_BRIGHTNESS, &led_cdev->work_flags);
337-
} else {
351+
else {
338352
clear_bit(LED_SET_BRIGHTNESS, &led_cdev->work_flags);
339353
clear_bit(LED_SET_BLINK, &led_cdev->work_flags);
340354
set_bit(LED_SET_BRIGHTNESS_OFF, &led_cdev->work_flags);

0 commit comments

Comments
 (0)