Linux LED subsystem development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Qiling Zhu" <kmatzkaczor244@gmail.com>
Cc: linux-leds@vger.kernel.org, lee@kernel.org
Subject: Re: [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
Date: Mon, 02 Nov 2026 15:33:04 +0000	[thread overview]
Message-ID: <sashiko-outbox-193671@kernel.org> (raw)
In-Reply-To: <20261102151211.40701-3-kmatzkaczor244@gmail.com>

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] platform/x86: thinkpad_acpi: bad kbdlight_auto_trigger_hw_offloaded() check
- [High] platform/x86: thinkpad_acpi: race in kbdlight_auto_trigger_activate()
- [Medium] platform/x86: thinkpad_acpi: unsigned enum check in kbdlight_init()
- [Medium] platform/x86: thinkpad_acpi: unchecked level in kbdlight_notify_change()
- [Medium] platform/x86: thinkpad_acpi: stale level when auto trigger activate fails
- [Medium] platform/x86: thinkpad_acpi: async race in kbdlight_set_level_and_update()
--

commit 58813975928928e80e772abf45a021a9664adf5b
Author: Qiling Zhu <kmatzkaczor244@gmail.com>

platform/x86: thinkpad_acpi: Add support for auto keyboard backlight

This commit adds support for the ambient light sensor (ALS) automatic
keyboard backlight mode (EC level 3) on ThinkPads by exposing a private
"tpacpi-kbd-auto" hardware control LED trigger and synchronizing EC level
changes with the LED core.

> diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
> index 3260eaa620a0e..7b46ad6f9618c 100644
> --- a/drivers/platform/x86/lenovo/thinkpad_acpi.c
> +++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c
> @@ -5102,6 +5102,14 @@ static struct ibm_struct video_driver_data = {
>   * Keyboard backlight subdriver
>   */
>  
> +/* EC level that selects ALS-controlled auto mode for the keyboard backlight */
> +#define TPACPI_KBD_LIGHT_AUTO_LEVEL	3
> +
> +/*
> + * Cached EC level, including TPACPI_KBD_LIGHT_AUTO_LEVEL. All state
> + * tracking compares EC levels; the auto mode is translated to a
> + * brightness value only when reporting to the LED core.
> + */
>  static enum led_brightness kbdlight_brightness;
>  static DEFINE_MUTEX(kbdlight_mutex);
>  
> @@ -5180,6 +5188,15 @@ static bool kbdlight_is_supported(void)
>  	return status & BIT(9);
>  }
>  
> +/* The auto mode is not a brightness level; report it as off to the LED core. */
> +static enum led_brightness kbdlight_level_to_brightness(int level)
> +{
> +	if (level == TPACPI_KBD_LIGHT_AUTO_LEVEL)
> +		return 0;
> +
> +	return level;
> +}
> +
>  static int kbdlight_sysfs_set(struct led_classdev *led_cdev,
>  			enum led_brightness brightness)
>  {
> @@ -5194,16 +5211,54 @@ static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
>  	if (level < 0)
>  		return 0;
>  
> -	return level;
> +	return kbdlight_level_to_brightness(level);
> +}
> +
> +static struct led_hw_trigger_type kbdlight_auto_trigger_type;
> +
> +static int kbdlight_auto_trigger_activate(struct led_classdev *led_cdev)
> +{
> +	/*
> +	 * The EC may already be in auto mode (cycled there on Fn+Space, or
> +	 * active since before the driver loaded), in which case there is
> +	 * nothing to program.
> +	 */
> +	if (kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL)
> +		return 0;
> +
> +	if (kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL))
> +		return -EIO;

[Severity: High]
Can a rapid Fn+Space sequence race with the asynchronous trigger activation
worker here and leave the EC stuck in auto mode while the trigger is detached?

When Fn+Space cycles into auto mode (level 3), kbdlight_notify_change() calls
led_trigger_notify_hw_control_changed(led_cdev, true), which schedules
trigger_hw_changed_work. Contrary to the comment in kbdlight_notify_change(),
led_trigger_hw_control_changed_worker() in drivers/leds/led-triggers.c does
not call hw_offloaded(); instead it runs:

  led_trigger_hw_control_changed_worker()
    led_trigger_do_hw_control_transition()
      __led_trigger_set(led_cdev, hc_trig, true)
        synchronize_rcu()
        flush_work(&led_cdev->set_brightness_work)
        kbdlight_auto_trigger_activate()

If a second Fn+Space press cycles the EC from level 3 to level 0 while the
first transition is pending or sleeping in synchronize_rcu(),
kbdlight_notify_change() sets trigger_hw_changed = false and re-queues
trigger_hw_changed_work.

When the first worker reaches kbdlight_auto_trigger_activate() in
drivers/platform/x86/lenovo/thinkpad_acpi.c, kbdlight_get_level() returns 0,
so kbdlight_auto_trigger_activate() calls
kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL) and programs the EC back to
level 3. Immediately afterwards, the re-queued worker runs with
activate = false and calls __led_trigger_set(led_cdev, NULL, true), which
detaches the trigger without changing the hardware level (because
hw_triggered is true), leaving the hardware in auto mode while sysfs reports
the [none] trigger.

> +
> +	/*
> +	 * Check that the EC actually accepted the auto mode; on error the
> +	 * trigger core reverts the LED to the "none" trigger.
> +	 */
> +	if (kbdlight_get_level() != TPACPI_KBD_LIGHT_AUTO_LEVEL)
> +		return -EIO;

[Severity: Medium]
If the EC does not support auto mode and rejects level 3 when a user selects
tpacpi-kbd-auto via sysfs, does this leave kbdlight_brightness corrupted with
TPACPI_KBD_LIGHT_AUTO_LEVEL?

In drivers/platform/x86/lenovo/thinkpad_acpi.c, kbdlight_init() registers
kbdlight_auto_trigger unconditionally on all ThinkPads with a keyboard
backlight. When kbdlight_auto_trigger_activate() calls
kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL), kbdlight_set_level()
unconditionally updates kbdlight_brightness:

	if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level))
		return -EIO;

	kbdlight_brightness = level;
	return 0;

When kbdlight_get_level() != TPACPI_KBD_LIGHT_AUTO_LEVEL then fails and
returns -EIO, kbdlight_brightness remains set to 3. Because __led_trigger_set()
only queues set_brightness_work asynchronously on activation failure (and skips
it if LED_SUSPENDED is set), a concurrent or subsequent kbdlight_notify_change()
call can observe kbdlight_brightness == 3 while the EC is at a non-auto level
and emit spurious hardware control and brightness change notifications.

> +
> +	return 0;
> +}
> +
> +static bool kbdlight_auto_trigger_hw_offloaded(struct led_classdev *led_cdev)
> +{
> +	return kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +}

[Severity: High]
Does returning false from kbdlight_auto_trigger_hw_offloaded() when the EC
is no longer at level 3 prevent the trigger from being removed?

In drivers/leds/led-triggers.c, __led_trigger_is_hw_controlled() already
checks that led_cdev->trigger is attached and matches
led_cdev->hw_control_trigger before calling trig->hw_offloaded(led_cdev).
The hw_offloaded() callback is meant to distinguish hardware offload from
software fallback for hybrid triggers, rather than re-polling whether the
hardware is still in auto mode.

Because kbdlight_auto_trigger_hw_offloaded() in
drivers/platform/x86/lenovo/thinkpad_acpi.c returns false as soon as the EC
leaves level 3, __led_trigger_is_hw_controlled() also returns false even
though kbdlight_auto_trigger is still the active trigger on led_cdev:

1. If CONFIG_LEDS_TRIGGERS_HW_CHANGED is disabled (THINKPAD_ACPI only selects
   LEDS_TRIGGERS), pressing Fn+Space to exit auto mode changes the EC level to
   0 while led_cdev->trigger remains kbdlight_auto_trigger. Subsequent writes
   to /sys/class/leds/tpacpi::kbd_backlight/brightness (via
   led_trigger_remove_hw_control()) or /proc/acpi/ibm/kbdlight (via
   kbdlight_set_level_and_update()) see led_trigger_is_hw_controlled() == false
   and never detach the trigger.

2. Even with CONFIG_LEDS_TRIGGERS_HW_CHANGED enabled, if the EC leaves level 3
   (for example across suspend/resume or via Fn+Space before the async notification
   runs) and userspace writes the matching level to /proc/acpi/ibm/kbdlight
   before kbdlight_notify_change() executes, kbdlight_set_level_and_update()
   sees led_trigger_is_hw_controlled() == false, skips
   led_trigger_remove_hw_control(), and calls kbdlight_set_level(level),
   updating kbdlight_brightness = level. When kbdlight_notify_change() runs
   next, kbdlight_brightness == level is already true, so it returns early
   without calling led_trigger_notify_hw_control_changed(led_cdev, false),
   leaving the LED stuck on tpacpi-kbd-auto.

[ ... ]

> @@ -5221,9 +5276,30 @@ static int __init kbdlight_init(struct ibm_init_struct *iibm)
>  		return -ENODEV;
>  	}
>  
> -	kbdlight_brightness = kbdlight_sysfs_get(NULL);
> +	kbdlight_brightness = kbdlight_get_level();
> +	if (kbdlight_brightness < 0)
> +		kbdlight_brightness = 0;

[Severity: Medium]
Since kbdlight_brightness is declared as enum led_brightness, is the
kbdlight_brightness < 0 check in kbdlight_init() always false?

In include/linux/leds.h, enum led_brightness only defines non-negative
constants (LED_OFF = 0 through LED_FULL = 255), so its underlying type is
unsigned int. Previously, kbdlight_sysfs_get() stored the return value of
kbdlight_get_level() in a signed int local variable and checked level < 0
before returning. Assigning kbdlight_get_level() directly to
kbdlight_brightness in drivers/platform/x86/lenovo/thinkpad_acpi.c means a
negative error code from kbdlight_get_level() wraps to a large unsigned value
and is not clamped to 0.

[ ... ]

> @@ -5246,9 +5322,22 @@ static int kbdlight_set_level_and_update(int level)
>  	int ret;
>  	struct led_classdev *led_cdev;
>  
> -	ret = kbdlight_set_level(level);
>  	led_cdev = &tpacpi_led_kbdlight.led_classdev;
>  
> +	/*
> +	 * Leaving auto mode through the proc interface has to drop the
> +	 * trigger, like writing a brightness through sysfs does. Route the
> +	 * new level through the LED core as well, so that it is applied
> +	 * after the LED_OFF queued by dropping the trigger.
> +	 */
> +	if (led_trigger_is_hw_controlled(led_cdev)) {
> +		led_trigger_remove_hw_control(led_cdev);
> +		led_set_brightness(led_cdev, level);
> +		return 0;
> +	}
> +
> +	ret = kbdlight_set_level(level);
> +
>  	if (ret == 0 && !(led_cdev->flags & LED_SUSPENDED))
>  		led_cdev->brightness = level;

[Severity: Medium]
Because tpacpi_led_kbdlight uses brightness_set_blocking, calling
led_trigger_remove_hw_control() and led_set_brightness() in
kbdlight_set_level_and_update() clears led_cdev->trigger and queues
set_brightness_work on leds_wq asynchronously without holding
led_cdev->led_access or flushing set_brightness_work.

Could this race with a second procfs write or with kbdlight_suspend()?

If a second write to /proc/acpi/ibm/kbdlight arrives before
set_brightness_work runs, led_trigger_is_hw_controlled(led_cdev) is already
false, so the second write calls kbdlight_set_level() synchronously, and
set_brightness_work from the first write then overwrites the EC level out of
order. Similarly, if kbdlight_suspend() runs before set_brightness_work
executes, kbdlight_get_level() in kbdlight_suspend() still sees
TPACPI_KBD_LIGHT_AUTO_LEVEL and returns early without calling
led_classdev_suspend(), allowing the pending work to program the backlight
during suspend.

[ ... ]

> @@ -5301,38 +5392,75 @@ static void kbdlight_suspend(void)

[ ... ]

>  static void kbdlight_notify_change(void)
>  {
>  	struct led_classdev *led_cdev = &tpacpi_led_kbdlight.led_classdev;
>  	enum led_brightness brightness;
> +	bool was_auto, is_auto;
> +	int level;
>  
>  	guard(mutex)(&kbdlight_mutex);
>  
>  	/*
> -	 * Check the brightness actually changed, setting the brightness
> -	 * through kbdlight_set_level() also triggers this event.
> +	 * Check the level actually changed, setting the brightness through
> +	 * kbdlight_set_level() also triggers this event.
>  	 */
> -	brightness = kbdlight_sysfs_get(NULL);
> -	if (kbdlight_brightness == brightness)
> +	level = kbdlight_get_level();
> +	if (kbdlight_brightness == level)
>  		return;
>  
> -	kbdlight_brightness = brightness;
> +	/*
> +	 * Notifying the trigger core is safe while holding
> +	 * kbdlight_mutex: the transition is only queued, and the worker
> +	 * re-reads the EC level (via hw_offloaded) after this critical
> +	 * section has been left.
> +	 */
> +	is_auto = level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +	was_auto = kbdlight_brightness == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +	if (is_auto != was_auto)
> +		led_trigger_notify_hw_control_changed(led_cdev, is_auto);
> +
> +	brightness = kbdlight_level_to_brightness(level);
> +	kbdlight_brightness = level;
>  	led_classdev_notify_brightness_hw_changed(led_cdev, brightness);
>  }

[Severity: Medium]
What happens in kbdlight_notify_change() if kbdlight_get_level() returns a
negative error code on a hotkey notification or during kbdlight_resume()?

Previously, kbdlight_notify_change() called kbdlight_sysfs_get(NULL), which
checked if (level < 0). Now kbdlight_get_level() is called directly without an
error check. If kbdlight_get_level() fails and returns a negative value (such
as -EIO or -ENXIO):

1. is_auto evaluates to false, so if was_auto was true,
   led_trigger_notify_hw_control_changed(led_cdev, false) is called and drops
   the active auto trigger.

2. kbdlight_level_to_brightness(level) returns the negative error value cast to
   enum led_brightness, which is then stored in kbdlight_brightness and passed
   to led_classdev_notify_brightness_hw_changed(led_cdev, brightness), exposing
   a wrapped unsigned error value through brightness_hw_changed in sysfs.

Should kbdlight_notify_change() bail out when level < 0?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261102151211.40701-1-kmatzkaczor244@gmail.com?part=2

  reply	other threads:[~2026-11-02 15:33 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-01 10:01 [RFC PATCH 0/1] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight Qiling Zhu
2026-11-01 10:01 ` [RFC PATCH 1/1] " Qiling Zhu
2026-11-01 10:22   ` sashiko-bot
2026-11-01 18:24   ` Rong Zhang
2026-11-01 17:57 ` [RFC PATCH 0/1] " Rong Zhang
2026-11-02 15:10 ` [RFC PATCH v2 0/2] " Qiling Zhu
2026-11-02 15:10   ` [RFC PATCH v2 1/2] platform/x86: thinkpad_acpi: Extract kbdlight_notify_change() helper Qiling Zhu
2026-11-02 15:16     ` sashiko-bot
2026-11-02 15:10   ` [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight Qiling Zhu
2026-11-02 15:33     ` sashiko-bot [this message]
2026-11-02 18:32     ` Rong Zhang
2026-11-02 18:13   ` [RFC PATCH v2 0/2] " Rong Zhang

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-193671@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=kmatzkaczor244@gmail.com \
    --cc=lee@kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox