* [RFC PATCH 0/1] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
@ 2026-11-01 10:01 Qiling Zhu
2026-11-01 10:01 ` [RFC PATCH 1/1] " Qiling Zhu
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Qiling Zhu @ 2026-11-01 10:01 UTC (permalink / raw)
To: Mark Pearson, Derek J. Clark, Rong Zhang, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar,
Qiling Zhu
Hi Mark, Ilpo, Rong,
Recent ThinkPads with an ambient light sensor (ALS) support an auto
mode for the keyboard backlight: the EC picks the backlight level
according to ambient light. The mode is EC keyboard backlight level 3,
above the two fixed levels exposed so far, and it is reachable only
through the Fn+Space shortcut, which is handled entirely by the
firmware. Userspace can neither select the mode nor observe whether it
is active.
This series wires the auto mode up as a private hardware control
trigger, "tpacpi-kbd-auto", on top of the hw control trigger framework
that recently landed in mainline [1]. With it:
- selecting the trigger writes EC level 3 (auto mode on); writing
"none" turns the backlight off
- the trigger state (bracketed in the trigger sysfs attribute, and
the new trigger_may_offload_to_hw attribute) is the userspace
view of auto mode; reading brightness returns -ENODATA while
auto mode is active, per the framework semantics
- Fn+Space keeps cycling through levels and auto mode as before,
and the trigger state now follows hardware-initiated transitions
automatically
This is the thinkpad_acpi counterpart of the ideapad-laptop wiring
from the same framework series, which is still queued; this patch
depends only on the LED core parts already in mainline and can be
applied independently.
For background: the February discussion settled that auto mode must
not be exposed as a brightness value [2], but no agreement was
reached back then on how to expose it instead; the hardware control
trigger approach was established later by the framework series [1].
Tested on a ThinkPad X1 Carbon Gen 14 (MT 21V8) running linux-next
next-20261030: 61 scripted user-visible assertions covering the
software paths, the Fn+Space event chain and the unchanged legacy
behavior, plus manual exercising of both the Fn+Space cycling and
desktop-environment brightness controls; checkpatch --strict clean.
Deliberately unchanged (details in the commit message):
- max_brightness stays 2; auto mode is not multiplexed into the
brightness value
- writing 3 to /proc/acpi/ibm/kbdlight keeps returning -EINVAL
- module unload still turns the keyboard backlight off
Two open questions, the main reason for the RFC tag:
1. MLCG returns capability bits in the upper 16 bits; does one of
them indicate auto mode support? If yes, I would gate trigger
registration on it. Mark, does the EC documentation say?
2. On models without an ALS, the EC behavior when asked for level 3
via MLCS is unknown to me. If it is undefined rather than
rejected, optimistic registration would need a rethink (e.g.
DMI whitelisting).
Rong, happy to rebase this on top of your series or fold it in, if
you prefer to keep both wirings together.
Comments welcome.
Regards,
Qiling Zhu
This patch was developed with LLM assistance; I reviewed every design
decision against the framework sources and validated the behavior on
real hardware myself.
[1] https://lore.kernel.org/r/20260618-leds-trigger-hw-changed-v2-0-c28c44053cf3@rong.moe/
[2] https://lore.kernel.org/r/20260203232219.11683-1-vishnuocv@gmail.com/
Qiling Zhu (1):
platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
drivers/platform/x86/lenovo/Kconfig | 1 +
drivers/platform/x86/lenovo/thinkpad_acpi.c | 104 ++++++++++++++++----
2 files changed, 88 insertions(+), 17 deletions(-)
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [RFC PATCH 1/1] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
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 ` 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
2 siblings, 2 replies; 12+ messages in thread
From: Qiling Zhu @ 2026-11-01 10:01 UTC (permalink / raw)
To: Mark Pearson, Derek J. Clark, Rong Zhang, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar,
Qiling Zhu
Recent ThinkPads with an ambient light sensor (ALS) support an auto mode
for the keyboard backlight: the EC controls the backlight according to
ambient light. The mode is EC keyboard backlight level 3, in addition to
the two fixed brightness levels currently exposed by the driver, and is
currently reachable only through the Fn+Space shortcut, which is handled
directly by the firmware.
Wire the auto mode up as a private hardware control trigger, named
"tpacpi-kbd-auto", on top of the LED hw control trigger framework:
- selecting the trigger writes level 3 (activate); the offloaded state
is derived from the current EC level
- when Fn+Space cycles into or out of auto mode, the driver notifies
the LED core about the hardware-initiated hardware control
transition, so the trigger state (shown bracketed in the trigger
sysfs attribute) follows along
- if the hardware is already in auto mode at driver initialization,
the trigger is set as the default one
Deliberately unchanged:
- max_brightness stays 2, auto mode is not multiplexed into the
brightness value
- writing 3 to /proc/acpi/ibm/kbdlight keeps returning -EINVAL, while
reads keep reporting the current EC level verbatim (including 3)
- module unload still turns the keyboard backlight off
Signed-off-by: Qiling Zhu <kmatzkaczor244@gmail.com>
---
Note: Documentation/admin-guide/laptops/thinkpad-acpi.rst has no kbdlight
section at all, so there is no in-tree documentation to update; the
userspace-facing semantics of hardware control triggers are documented
in Documentation/leds/leds-class.rst.
Open questions (details in the cover letter): whether a MLCG capability
bit indicates auto mode support, and the EC behavior on ALS-less models
when asked for level 3.
drivers/platform/x86/lenovo/Kconfig | 1 +
drivers/platform/x86/lenovo/thinkpad_acpi.c | 104 ++++++++++++++++----
2 files changed, 88 insertions(+), 17 deletions(-)
diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index 516594993..74fc66a44 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -75,6 +75,7 @@ config THINKPAD_ACPI
depends on RFKILL || RFKILL = n
depends on ACPI_VIDEO || ACPI_VIDEO = n
depends on BACKLIGHT_CLASS_DEVICE
+ depends on LEDS_TRIGGERS_HW_CHANGED
depends on I2C
depends on DRM
select ACPI_PLATFORM_PROFILE
diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
index 4f81855ad..cd5c17d5f 100644
--- a/drivers/platform/x86/lenovo/thinkpad_acpi.c
+++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c
@@ -5102,6 +5102,9 @@ 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
+
static enum led_brightness kbdlight_brightness;
static DEFINE_MUTEX(kbdlight_mutex);
@@ -5197,13 +5200,33 @@ static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
return level;
}
+static struct led_hw_trigger_type kbdlight_auto_trigger_type;
+
+static int kbdlight_auto_trigger_activate(struct led_classdev *led_cdev)
+{
+ return kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL);
+}
+
+static bool kbdlight_auto_trigger_hw_offloaded(struct led_classdev *led_cdev)
+{
+ return kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL;
+}
+
+static struct led_trigger kbdlight_auto_trigger = {
+ .name = "tpacpi-kbd-auto",
+ .trigger_type = &kbdlight_auto_trigger_type,
+ .activate = kbdlight_auto_trigger_activate,
+ .hw_offloaded = kbdlight_auto_trigger_hw_offloaded,
+};
+
static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
.led_classdev = {
.name = "tpacpi::kbd_backlight",
.max_brightness = 2,
- .flags = LED_BRIGHT_HW_CHANGED,
+ .flags = LED_BRIGHT_HW_CHANGED | LED_TRIG_HW_CHANGED,
.brightness_set_blocking = &kbdlight_sysfs_set,
.brightness_get = &kbdlight_sysfs_get,
+ .trigger_type = &kbdlight_auto_trigger_type,
}
};
@@ -5224,6 +5247,26 @@ static int __init kbdlight_init(struct ibm_init_struct *iibm)
kbdlight_brightness = kbdlight_sysfs_get(NULL);
tp_features.kbdlight = 1;
+ /*
+ * Register the auto-mode trigger before the LED classdev: if the
+ * hardware is already in auto mode, the trigger can then be set as
+ * the default one and gets activated during LED classdev
+ * registration.
+ */
+ rc = devm_led_trigger_register(&tpacpi_pdev->dev,
+ &kbdlight_auto_trigger);
+ if (rc) {
+ tp_features.kbdlight = 0;
+ return rc;
+ }
+
+ tpacpi_led_kbdlight.led_classdev.hw_control_trigger =
+ kbdlight_auto_trigger.name;
+
+ if (kbdlight_brightness == TPACPI_KBD_LIGHT_AUTO_LEVEL)
+ tpacpi_led_kbdlight.led_classdev.default_trigger =
+ kbdlight_auto_trigger.name;
+
rc = led_classdev_register(&tpacpi_pdev->dev,
&tpacpi_led_kbdlight.led_classdev);
if (rc < 0) {
@@ -5313,6 +5356,47 @@ static void kbdlight_resume(void)
led_classdev_resume(&tpacpi_led_kbdlight.led_classdev);
}
+/* Notify the LED core when the hardware cycles into or out of auto mode. */
+static void kbdlight_notify_hw_control(struct led_classdev *led_cdev,
+ int old_level, int new_level)
+{
+ bool was_auto = old_level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
+ bool is_auto = new_level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
+
+ if (is_auto != was_auto)
+ led_trigger_notify_hw_control_changed(led_cdev, is_auto);
+}
+
+/*
+ * The EC cycles the keyboard backlight level (and auto mode) itself on
+ * Fn+Space; check the current level and update the LED core state.
+ */
+static void kbdlight_notify_change(void)
+{
+ struct led_classdev *led_cdev = &tpacpi_led_kbdlight.led_classdev;
+ enum led_brightness brightness;
+
+ guard(mutex)(&kbdlight_mutex);
+
+ /*
+ * Check the brightness actually changed, setting the brightness
+ * through kbdlight_set_level() also triggers this event.
+ */
+ brightness = kbdlight_sysfs_get(NULL);
+ if (kbdlight_brightness == brightness)
+ return;
+
+ /*
+ * Calling kbdlight_notify_hw_control() is safe while holding
+ * kbdlight_mutex: the notification is only queued, and the worker
+ * re-acquires the mutex (if at all) after this critical section has
+ * been left.
+ */
+ kbdlight_notify_hw_control(led_cdev, kbdlight_brightness, brightness);
+ kbdlight_brightness = brightness;
+ led_classdev_notify_brightness_hw_changed(led_cdev, brightness);
+}
+
static struct ibm_struct kbdlight_driver_data = {
.name = "kbdlight",
.read = kbdlight_read,
@@ -11581,22 +11665,8 @@ static bool tpacpi_driver_event(const unsigned int hkey_event)
/* Key events are suppressed by default hotkey_user_mask */
return false;
case TP_HKEY_EV_KBD_LIGHT:
- if (tp_features.kbdlight) {
- enum led_brightness brightness;
-
- guard(mutex)(&kbdlight_mutex);
-
- /*
- * Check the brightness actually changed, setting the brightness
- * through kbdlight_set_level() also triggers this event.
- */
- brightness = kbdlight_sysfs_get(NULL);
- if (kbdlight_brightness != brightness) {
- kbdlight_brightness = brightness;
- led_classdev_notify_brightness_hw_changed(
- &tpacpi_led_kbdlight.led_classdev, brightness);
- }
- }
+ if (tp_features.kbdlight)
+ kbdlight_notify_change();
/* Key events are suppressed by default hotkey_user_mask */
return false;
case TP_HKEY_EV_DFR_CHANGE_ROW:
--
2.56.0
base-commit: 7649ba73f9e7e1e04d0b46213e39c79ae09dd55c
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [RFC PATCH 1/1] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
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
1 sibling, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-01 10:22 UTC (permalink / raw)
To: Qiling Zhu; +Cc: linux-leds, lee
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Medium] platform/x86: thinkpad_acpi: circular Kconfig dependency on THINKPAD_ACPI
- [Medium] platform/x86: thinkpad_acpi: auto level 3 exposed as LED brightness > max
- [Medium] platform/x86: thinkpad_acpi: procfs write in auto mode leaves trigger stuck
- [Medium] platform/x86: thinkpad_acpi: race in kbdlight_auto_trigger_activate() work
- [Medium] platform/x86: thinkpad_acpi: kbdlight_suspend() breaks auto trigger state
- [Medium] platform/x86: thinkpad_acpi: unchecked level 3 support in kbdlight_set_level()
--
commit 0036d7647f18bf2aea756a93b29c68f5fa29627e
Author: Qiling Zhu <kmatzkaczor244@gmail.com>
platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
This commit adds support for the EC-controlled automatic keyboard backlight
mode (level 3) on ThinkPads by registering a private "tpacpi-kbd-auto" LED
hardware control trigger and notifying the LED core when Fn+Space cycles
into or out of auto mode.
> diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
> index 5165949930739..74fc66a44c1e2 100644
> --- a/drivers/platform/x86/lenovo/Kconfig
> +++ b/drivers/platform/x86/lenovo/Kconfig
> @@ -75,6 +75,7 @@ config THINKPAD_ACPI
> depends on RFKILL || RFKILL = n
> depends on ACPI_VIDEO || ACPI_VIDEO = n
> depends on BACKLIGHT_CLASS_DEVICE
> + depends on LEDS_TRIGGERS_HW_CHANGED
[Severity: Medium]
Does adding "depends on LEDS_TRIGGERS_HW_CHANGED" to THINKPAD_ACPI in
drivers/platform/x86/lenovo/Kconfig introduce a circular Kconfig dependency?
In drivers/platform/x86/lenovo/Kconfig, THINKPAD_ACPI selects NEW_LEDS and
LEDS_CLASS:
select NEW_LEDS
select LEDS_CLASS
Meanwhile, in drivers/leds/Kconfig and drivers/leds/trigger/Kconfig,
LEDS_TRIGGERS_HW_CHANGED is defined inside "if NEW_LEDS" and "if
LEDS_TRIGGERS", where LEDS_TRIGGERS depends on LEDS_CLASS. This forms a
dependency cycle (THINKPAD_ACPI -> LEDS_TRIGGERS_HW_CHANGED -> LEDS_TRIGGERS
-> LEDS_CLASS -> THINKPAD_ACPI) that causes Kconfig dependency checking to
fail with a recursive dependency error.
Additionally, LEDS_TRIGGERS_HW_CHANGED defaults to
LEDS_BRIGHTNESS_HW_CHANGED (which defaults to n), so depending on it hides
THINKPAD_ACPI whenever LEDS_TRIGGERS_HW_CHANGED is disabled, even though
include/linux/leds.h provides a stub for
led_trigger_notify_hw_control_changed() when CONFIG_LEDS_TRIGGERS_HW_CHANGED
is not set. Since struct led_trigger and devm_led_trigger_register() are
guarded by CONFIG_LEDS_TRIGGERS, should THINKPAD_ACPI select LEDS_TRIGGERS
(or LEDS_TRIGGERS_HW_CHANGED) instead of depending on
LEDS_TRIGGERS_HW_CHANGED?
> diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
> index 4f81855ad721c..cd5c17d5f0877 100644
> --- a/drivers/platform/x86/lenovo/thinkpad_acpi.c
> +++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c
[ ... ]
> @@ -5197,13 +5200,33 @@ static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
> return level;
> }
>
> +static struct led_hw_trigger_type kbdlight_auto_trigger_type;
> +
> +static int kbdlight_auto_trigger_activate(struct led_classdev *led_cdev)
> +{
> + return kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL);
> +}
[Severity: Medium]
Can the asynchronous trigger_hw_changed_work race with a subsequent Fn+Space
press and overwrite the EC level back to TPACPI_KBD_LIGHT_AUTO_LEVEL (3)?
When Fn+Space cycles the EC from level 2 to level 3,
kbdlight_notify_change() sets kbdlight_brightness = 3 and calls
led_trigger_notify_hw_control_changed(led_cdev, true), which queues
led_cdev->trigger_hw_changed_work.
When led_trigger_hw_control_changed_worker() runs, it snapshots
activate = READ_ONCE(led_cdev->trigger_hw_changed) (true) and calls
__led_trigger_set(led_cdev, hc_trig, true), which sleeps in
synchronize_rcu() and flush_work(&led_cdev->set_brightness_work) before
invoking kbdlight_auto_trigger_activate().
If the user presses Fn+Space again to cycle from level 3 (auto) to level 0
(off) during this window:
1. If the EC transitions to level 0 before kbdlight_auto_trigger_activate()
runs and before the second TP_HKEY_EV_KBD_LIGHT event is handled,
kbdlight_auto_trigger_activate() calls kbdlight_set_level(3), forcing the
EC back to level 3 and keeping kbdlight_brightness at 3, so the second
kbdlight_notify_change() call sees kbdlight_brightness == 3 and drops the
transition to 0.
2. If the second kbdlight_notify_change() runs after the worker has already
read activate = true (for instance while __led_trigger_set() is in
synchronize_rcu()), kbdlight_notify_change() updates kbdlight_brightness
to 0, sets trigger_hw_changed = false, and re-queues
trigger_hw_changed_work. The first worker then calls
kbdlight_auto_trigger_activate() -> kbdlight_set_level(3), writing level
3 back to the EC and setting kbdlight_brightness = 3. The re-queued
worker then runs with activate = false and calls
__led_trigger_set(led_cdev, NULL, true), which detaches the trigger in
software without changing the hardware, leaving the EC stuck in auto mode
(level 3) while sysfs shows the trigger as [none].
Should kbdlight_auto_trigger_activate() avoid writing to the EC when the
activation was initiated by hardware (or when kbdlight_brightness is already
TPACPI_KBD_LIGHT_AUTO_LEVEL)?
> +
> +static bool kbdlight_auto_trigger_hw_offloaded(struct led_classdev *led_cdev)
> +{
> + return kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +}
[Severity: Medium]
How does kbdlight_auto_trigger_hw_offloaded() interact with
kbdlight_suspend() and kbdlight_resume() when the keyboard backlight is in
auto mode?
During system suspend, kbdlight_suspend() runs:
led_cdev = &tpacpi_led_kbdlight.led_classdev;
led_update_brightness(led_cdev);
led_classdev_suspend(led_cdev);
led_classdev_suspend() sets LED_SUSPENDED, calls
led_set_brightness_nopm(led_cdev, 0), and flushes set_brightness_work, which
calls kbdlight_sysfs_set(led_cdev, 0) -> kbdlight_set_level(0). This sets
both the EC hardware level and kbdlight_brightness to 0 while
led_cdev->trigger remains kbdlight_auto_trigger.
While suspended, and during resume after kbdlight_resume() ->
led_classdev_resume() clears LED_SUSPENDED and queues set_brightness_work
before that work executes, the EC level is 0, so
kbdlight_auto_trigger_hw_offloaded() returns false even though
tpacpi-kbd-auto is still the active trigger:
- Reading /sys/class/leds/tpacpi::kbd_backlight/brightness sees
led_trigger_is_hw_controlled() == false, calls
led_update_brightness(led_cdev) (overwriting led_cdev->brightness with 0),
and returns 0 instead of -ENODATA.
- Writing a non-zero brightness (1 or 2) to sysfs brightness calls
led_trigger_remove_hw_control(), which checks
__led_trigger_is_hw_controlled() -> kbdlight_auto_trigger_hw_offloaded(),
gets false, and fails to detach tpacpi-kbd-auto.
- Because kbdlight_suspend() overwrote kbdlight_brightness with 0 via
kbdlight_set_level(0), if the backlight exits auto mode to level 0 across
suspend/resume, kbdlight_notify_change() sees kbdlight_brightness == 0 and
does not notify the LED core that auto mode ended.
> +
> +static struct led_trigger kbdlight_auto_trigger = {
> + .name = "tpacpi-kbd-auto",
> + .trigger_type = &kbdlight_auto_trigger_type,
> + .activate = kbdlight_auto_trigger_activate,
> + .hw_offloaded = kbdlight_auto_trigger_hw_offloaded,
> +};
> +
> static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
> .led_classdev = {
> .name = "tpacpi::kbd_backlight",
> .max_brightness = 2,
> - .flags = LED_BRIGHT_HW_CHANGED,
> + .flags = LED_BRIGHT_HW_CHANGED | LED_TRIG_HW_CHANGED,
> .brightness_set_blocking = &kbdlight_sysfs_set,
> .brightness_get = &kbdlight_sysfs_get,
> + .trigger_type = &kbdlight_auto_trigger_type,
> }
> };
>
> @@ -5224,6 +5247,26 @@ static int __init kbdlight_init(struct ibm_init_struct *iibm)
> kbdlight_brightness = kbdlight_sysfs_get(NULL);
> tp_features.kbdlight = 1;
>
> + /*
> + * Register the auto-mode trigger before the LED classdev: if the
> + * hardware is already in auto mode, the trigger can then be set as
> + * the default one and gets activated during LED classdev
> + * registration.
> + */
> + rc = devm_led_trigger_register(&tpacpi_pdev->dev,
> + &kbdlight_auto_trigger);
> + if (rc) {
> + tp_features.kbdlight = 0;
> + return rc;
> + }
> +
> + tpacpi_led_kbdlight.led_classdev.hw_control_trigger =
> + kbdlight_auto_trigger.name;
[Severity: Medium]
What happens on ThinkPad models that have a keyboard backlight (so
kbdlight_is_supported() returns true) but do not support ALS auto mode (level
3) when userspace selects the tpacpi-kbd-auto trigger?
kbdlight_init() unconditionally registers kbdlight_auto_trigger on all
models with a keyboard backlight. When userspace writes "tpacpi-kbd-auto" to
/sys/class/leds/tpacpi::kbd_backlight/trigger,
kbdlight_auto_trigger_activate() calls
kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL):
static int kbdlight_set_level(int level)
{
if (!hkey_handle)
return -ENXIO;
guard(mutex)(&kbdlight_mutex);
if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level))
return -EIO;
kbdlight_brightness = level;
return 0;
}
Unlike kbdlight_get_level() and kbdlight_is_supported(), kbdlight_set_level()
passes NULL for the MLCS return value (so a negative ACPI method error status
is ignored) and does not verify via kbdlight_get_level() that the EC
actually entered level 3. If the EC rejects or ignores level 3:
1. kbdlight_set_level(3) still sets kbdlight_brightness = 3 and returns 0,
so __led_trigger_set() marks tpacpi-kbd-auto as the active trigger.
2. kbdlight_auto_trigger_hw_offloaded() checks
kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL, which returns false
because the hardware stayed at its previous level (0, 1, or 2).
3. Subsequent non-zero writes to sysfs brightness call
led_trigger_remove_hw_control(), which checks
__led_trigger_is_hw_controlled(), gets false, and fails to remove the
active tpacpi-kbd-auto trigger.
4. When TP_HKEY_EV_KBD_LIGHT fires, kbdlight_notify_change() compares the
stale kbdlight_brightness (3) against kbdlight_sysfs_get(NULL) and treats
it as a hardware-initiated transition out of auto mode.
Should kbdlight_set_level() (or kbdlight_auto_trigger_activate()) check the
MLCS return status and verify that kbdlight_get_level() transitioned to the
requested level before updating kbdlight_brightness and succeeding?
[ ... ]
> @@ -5313,6 +5356,47 @@ static void kbdlight_resume(void)
> led_classdev_resume(&tpacpi_led_kbdlight.led_classdev);
> }
>
> +/* Notify the LED core when the hardware cycles into or out of auto mode. */
> +static void kbdlight_notify_hw_control(struct led_classdev *led_cdev,
> + int old_level, int new_level)
> +{
> + bool was_auto = old_level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> + bool is_auto = new_level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +
> + if (is_auto != was_auto)
> + led_trigger_notify_hw_control_changed(led_cdev, is_auto);
> +}
[Severity: Medium]
If the keyboard backlight is currently in auto mode (level 3) and a fixed
brightness level (0, 1, or 2) is written via /proc/acpi/ibm/kbdlight, doesn't
the tpacpi-kbd-auto trigger remain stuck as the active trigger?
Writing 0, 1, or 2 to /proc/acpi/ibm/kbdlight calls kbdlight_write() ->
kbdlight_set_level_and_update(level) -> kbdlight_set_level(level).
kbdlight_set_level() evaluates MLCS to switch the EC out of auto mode and
updates kbdlight_brightness = level under kbdlight_mutex, without calling
kbdlight_notify_hw_control() or removing the hardware control trigger.
When the EC then raises TP_HKEY_EV_KBD_LIGHT for that MLCS call,
kbdlight_notify_change() sees brightness == kbdlight_brightness and returns
early without calling kbdlight_notify_hw_control().
Furthermore, because the EC is now at level 0, 1, or 2,
kbdlight_auto_trigger_hw_offloaded() returns false. If userspace then writes
a non-zero brightness (1 or 2) to
/sys/class/leds/tpacpi::kbd_backlight/brightness, brightness_store() calls
led_trigger_remove_hw_control(), which checks
__led_trigger_is_hw_controlled() -> kbdlight_auto_trigger_hw_offloaded(),
sees false, and leaves tpacpi-kbd-auto attached (the same thing can also
happen if userspace writes 1 or 2 to sysfs brightness in the asynchronous
window after Fn+Space exits auto mode before trigger_hw_changed_work runs).
Should kbdlight_set_level_and_update() (or kbdlight_set_level()) notify or
remove the hardware control trigger when transitioning out of
TPACPI_KBD_LIGHT_AUTO_LEVEL?
> +
> +/*
> + * The EC cycles the keyboard backlight level (and auto mode) itself on
> + * Fn+Space; check the current level and update the LED core state.
> + */
> +static void kbdlight_notify_change(void)
> +{
> + struct led_classdev *led_cdev = &tpacpi_led_kbdlight.led_classdev;
> + enum led_brightness brightness;
> +
> + guard(mutex)(&kbdlight_mutex);
> +
> + /*
> + * Check the brightness actually changed, setting the brightness
> + * through kbdlight_set_level() also triggers this event.
> + */
> + brightness = kbdlight_sysfs_get(NULL);
> + if (kbdlight_brightness == brightness)
> + return;
> +
> + /*
> + * Calling kbdlight_notify_hw_control() is safe while holding
> + * kbdlight_mutex: the notification is only queued, and the worker
> + * re-acquires the mutex (if at all) after this critical section has
> + * been left.
> + */
> + kbdlight_notify_hw_control(led_cdev, kbdlight_brightness, brightness);
> + kbdlight_brightness = brightness;
> + led_classdev_notify_brightness_hw_changed(led_cdev, brightness);
> +}
[Severity: Medium]
Doesn't this expose TPACPI_KBD_LIGHT_AUTO_LEVEL (3), which exceeds
max_brightness (2), through both brightness_hw_changed and brightness?
The commit message notes that max_brightness stays 2 and auto mode is not
multiplexed into the brightness value. However:
1. In kbdlight_notify_change(), when Fn+Space cycles the EC into auto mode,
kbdlight_sysfs_get(NULL) returns TPACPI_KBD_LIGHT_AUTO_LEVEL (3), and
led_classdev_notify_brightness_hw_changed(led_cdev, brightness) is called
unconditionally with brightness == 3. This stores 3 in
led_cdev->brightness_hw_changed and notifies userspace pollers (such as
upower) of a brightness value greater than max_brightness.
2. Because kbdlight_sysfs_get() (the led_cdev->brightness_get callback)
returns kbdlight_get_level() verbatim, led_update_brightness() sets
led_cdev->brightness = 3 during led_classdev_register() when booting in
auto mode (before led_trigger_set_default() activates the trigger), during
kbdlight_suspend() when suspending in auto mode, and whenever userspace
reads /sys/class/leds/tpacpi::kbd_backlight/brightness while the EC is at
level 3 and led_trigger_is_hw_controlled() is false (such as in the
asynchronous window after Fn+Space before trigger_hw_changed_work runs, or
when a software trigger is attached). When Fn+Space later cycles from auto
mode (3) to off (0), __led_trigger_set(led_cdev, NULL, true) does not
update led_cdev->brightness, leaving it at 3.
Should kbdlight_notify_change() skip
led_classdev_notify_brightness_hw_changed() when entering
TPACPI_KBD_LIGHT_AUTO_LEVEL, and should kbdlight_sysfs_get() avoid returning
TPACPI_KBD_LIGHT_AUTO_LEVEL as an enum led_brightness to the LED core?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101100123.28749-1-kmatzkaczor244@gmail.com?part=1
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [RFC PATCH 0/1] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
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 17:57 ` Rong Zhang
2026-11-02 15:10 ` [RFC PATCH v2 0/2] " Qiling Zhu
2 siblings, 0 replies; 12+ messages in thread
From: Rong Zhang @ 2026-11-01 17:57 UTC (permalink / raw)
To: Qiling Zhu, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Vishnu Sankar, Vishnu Sankar, Griffin Kroah-Hartman,
ibm-acpi-devel, Lee Jones, linux-kernel, linux-leds, Pavel Machek,
platform-driver-x86, vishnuocv, vsankar
Hi Qiling,
Thanks for working on this.
On Sun, 2026-11-01 at 18:01 +0800, Qiling Zhu wrote:
> Hi Mark, Ilpo, Rong,
>
> Recent ThinkPads with an ambient light sensor (ALS) support an auto
> mode for the keyboard backlight: the EC picks the backlight level
> according to ambient light. The mode is EC keyboard backlight level 3,
> above the two fixed levels exposed so far, and it is reachable only
> through the Fn+Space shortcut, which is handled entirely by the
> firmware. Userspace can neither select the mode nor observe whether it
> is active.
>
> This series wires the auto mode up as a private hardware control
> trigger, "tpacpi-kbd-auto", on top of the hw control trigger framework
> that recently landed in mainline [1]. With it:
>
> - selecting the trigger writes EC level 3 (auto mode on); writing
> "none" turns the backlight off
> - the trigger state (bracketed in the trigger sysfs attribute, and
> the new trigger_may_offload_to_hw attribute) is the userspace
> view of auto mode; reading brightness returns -ENODATA while
> auto mode is active, per the framework semantics
> - Fn+Space keeps cycling through levels and auto mode as before,
> and the trigger state now follows hardware-initiated transitions
> automatically
>
> This is the thinkpad_acpi counterpart of the ideapad-laptop wiring
> from the same framework series, which is still queued; this patch
> depends only on the LED core parts already in mainline and can be
> applied independently.
>
> For background: the February discussion settled that auto mode must
> not be exposed as a brightness value [2], but no agreement was
> reached back then on how to expose it instead; the hardware control
> trigger approach was established later by the framework series [1].
>
> Tested on a ThinkPad X1 Carbon Gen 14 (MT 21V8) running linux-next
> next-20261030: 61 scripted user-visible assertions covering the
> software paths, the Fn+Space event chain and the unchanged legacy
> behavior, plus manual exercising of both the Fn+Space cycling and
> desktop-environment brightness controls; checkpatch --strict clean.
>
> Deliberately unchanged (details in the commit message):
>
> - max_brightness stays 2; auto mode is not multiplexed into the
> brightness value
> - writing 3 to /proc/acpi/ibm/kbdlight keeps returning -EINVAL
> - module unload still turns the keyboard backlight off
>
> Two open questions, the main reason for the RFC tag:
>
> 1. MLCG returns capability bits in the upper 16 bits; does one of
> them indicate auto mode support? If yes, I would gate trigger
> registration on it. Mark, does the EC documentation say?
> 2. On models without an ALS, the EC behavior when asked for level 3
> via MLCS is unknown to me. If it is undefined rather than
> rejected, optimistic registration would need a rethink (e.g.
> DMI whitelisting).
>
> Rong, happy to rebase this on top of your series or fold it in, if
> you prefer to keep both wirings together.
(+CC Vishnu)
AFAIK, Vishnu is also working on adding the same support using the hw
control trigger framework. Maybe they can answer your questions or
integrate your patch into theirs.
Thanks,
Rong
>
> Comments welcome.
>
> Regards,
> Qiling Zhu
>
> This patch was developed with LLM assistance; I reviewed every design
> decision against the framework sources and validated the behavior on
> real hardware myself.
>
> [1] https://lore.kernel.org/r/20260618-leds-trigger-hw-changed-v2-0-c28c44053cf3@rong.moe/
> [2] https://lore.kernel.org/r/20260203232219.11683-1-vishnuocv@gmail.com/
>
> Qiling Zhu (1):
> platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
>
> drivers/platform/x86/lenovo/Kconfig | 1 +
> drivers/platform/x86/lenovo/thinkpad_acpi.c | 104 ++++++++++++++++----
> 2 files changed, 88 insertions(+), 17 deletions(-)
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [RFC PATCH 1/1] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
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
1 sibling, 0 replies; 12+ messages in thread
From: Rong Zhang @ 2026-11-01 18:24 UTC (permalink / raw)
To: Qiling Zhu, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar
Hi Qiling
On Sun, 2026-11-01 at 18:01 +0800, Qiling Zhu wrote:
> Recent ThinkPads with an ambient light sensor (ALS) support an auto mode
> for the keyboard backlight: the EC controls the backlight according to
> ambient light. The mode is EC keyboard backlight level 3, in addition to
> the two fixed brightness levels currently exposed by the driver, and is
> currently reachable only through the Fn+Space shortcut, which is handled
> directly by the firmware.
>
> Wire the auto mode up as a private hardware control trigger, named
> "tpacpi-kbd-auto", on top of the LED hw control trigger framework:
>
> - selecting the trigger writes level 3 (activate); the offloaded state
> is derived from the current EC level
> - when Fn+Space cycles into or out of auto mode, the driver notifies
> the LED core about the hardware-initiated hardware control
> transition, so the trigger state (shown bracketed in the trigger
> sysfs attribute) follows along
> - if the hardware is already in auto mode at driver initialization,
> the trigger is set as the default one
>
> Deliberately unchanged:
>
> - max_brightness stays 2, auto mode is not multiplexed into the
> brightness value
> - writing 3 to /proc/acpi/ibm/kbdlight keeps returning -EINVAL, while
> reads keep reporting the current EC level verbatim (including 3)
> - module unload still turns the keyboard backlight off
>
> Signed-off-by: Qiling Zhu <kmatzkaczor244@gmail.com>
> ---
>
> Note: Documentation/admin-guide/laptops/thinkpad-acpi.rst has no kbdlight
> section at all, so there is no in-tree documentation to update; the
> userspace-facing semantics of hardware control triggers are documented
> in Documentation/leds/leds-class.rst.
>
> Open questions (details in the cover letter): whether a MLCG capability
> bit indicates auto mode support, and the EC behavior on ALS-less models
> when asked for level 3.
>
> drivers/platform/x86/lenovo/Kconfig | 1 +
> drivers/platform/x86/lenovo/thinkpad_acpi.c | 104 ++++++++++++++++----
> 2 files changed, 88 insertions(+), 17 deletions(-)
>
> diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
> index 516594993..74fc66a44 100644
> --- a/drivers/platform/x86/lenovo/Kconfig
> +++ b/drivers/platform/x86/lenovo/Kconfig
> @@ -75,6 +75,7 @@ config THINKPAD_ACPI
> depends on RFKILL || RFKILL = n
> depends on ACPI_VIDEO || ACPI_VIDEO = n
> depends on BACKLIGHT_CLASS_DEVICE
> + depends on LEDS_TRIGGERS_HW_CHANGED
Nah. No driver should depend on or select LEDS_TRIGGERS_HW_CHANGED.
Everything still works without it.
That's also why no driver depends on or selects
LEDS_BRIGHTNESS_HW_CHANGED.
Both options are only enabled if a user (or a distribution) opts into it
while configuring Kconfig options.
> depends on I2C
> depends on DRM
> select ACPI_PLATFORM_PROFILE
> diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
> index 4f81855ad..cd5c17d5f 100644
> --- a/drivers/platform/x86/lenovo/thinkpad_acpi.c
> +++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c
> @@ -5102,6 +5102,9 @@ 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
> +
> static enum led_brightness kbdlight_brightness;
> static DEFINE_MUTEX(kbdlight_mutex);
>
> @@ -5197,13 +5200,33 @@ static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
> return level;
> }
>
> +static struct led_hw_trigger_type kbdlight_auto_trigger_type;
> +
> +static int kbdlight_auto_trigger_activate(struct led_classdev *led_cdev)
> +{
> + return kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL);
> +}
> +
> +static bool kbdlight_auto_trigger_hw_offloaded(struct led_classdev *led_cdev)
> +{
> + return kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +}
> +
> +static struct led_trigger kbdlight_auto_trigger = {
> + .name = "tpacpi-kbd-auto",
> + .trigger_type = &kbdlight_auto_trigger_type,
> + .activate = kbdlight_auto_trigger_activate,
> + .hw_offloaded = kbdlight_auto_trigger_hw_offloaded,
> +};
> +
> static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
> .led_classdev = {
> .name = "tpacpi::kbd_backlight",
> .max_brightness = 2,
> - .flags = LED_BRIGHT_HW_CHANGED,
> + .flags = LED_BRIGHT_HW_CHANGED | LED_TRIG_HW_CHANGED,
> .brightness_set_blocking = &kbdlight_sysfs_set,
> .brightness_get = &kbdlight_sysfs_get,
> + .trigger_type = &kbdlight_auto_trigger_type,
> }
> };
>
> @@ -5224,6 +5247,26 @@ static int __init kbdlight_init(struct ibm_init_struct *iibm)
> kbdlight_brightness = kbdlight_sysfs_get(NULL);
> tp_features.kbdlight = 1;
>
> + /*
> + * Register the auto-mode trigger before the LED classdev: if the
> + * hardware is already in auto mode, the trigger can then be set as
> + * the default one and gets activated during LED classdev
> + * registration.
> + */
> + rc = devm_led_trigger_register(&tpacpi_pdev->dev,
> + &kbdlight_auto_trigger);
> + if (rc) {
> + tp_features.kbdlight = 0;
> + return rc;
> + }
> +
> + tpacpi_led_kbdlight.led_classdev.hw_control_trigger =
> + kbdlight_auto_trigger.name;
> +
> + if (kbdlight_brightness == TPACPI_KBD_LIGHT_AUTO_LEVEL)
> + tpacpi_led_kbdlight.led_classdev.default_trigger =
> + kbdlight_auto_trigger.name;
> +
> rc = led_classdev_register(&tpacpi_pdev->dev,
> &tpacpi_led_kbdlight.led_classdev);
> if (rc < 0) {
> @@ -5313,6 +5356,47 @@ static void kbdlight_resume(void)
> led_classdev_resume(&tpacpi_led_kbdlight.led_classdev);
> }
>
> +/* Notify the LED core when the hardware cycles into or out of auto mode. */
> +static void kbdlight_notify_hw_control(struct led_classdev *led_cdev,
> + int old_level, int new_level)
> +{
> + bool was_auto = old_level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> + bool is_auto = new_level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +
> + if (is_auto != was_auto)
> + led_trigger_notify_hw_control_changed(led_cdev, is_auto);
> +}
> +
> +/*
> + * The EC cycles the keyboard backlight level (and auto mode) itself on
> + * Fn+Space; check the current level and update the LED core state.
> + */
> +static void kbdlight_notify_change(void)
> +{
> + struct led_classdev *led_cdev = &tpacpi_led_kbdlight.led_classdev;
> + enum led_brightness brightness;
> +
> + guard(mutex)(&kbdlight_mutex);
> +
> + /*
> + * Check the brightness actually changed, setting the brightness
> + * through kbdlight_set_level() also triggers this event.
> + */
> + brightness = kbdlight_sysfs_get(NULL);
> + if (kbdlight_brightness == brightness)
> + return;
> +
> + /*
> + * Calling kbdlight_notify_hw_control() is safe while holding
> + * kbdlight_mutex: the notification is only queued, and the worker
> + * re-acquires the mutex (if at all) after this critical section has
> + * been left.
> + */
> + kbdlight_notify_hw_control(led_cdev, kbdlight_brightness, brightness);
> + kbdlight_brightness = brightness;
> + led_classdev_notify_brightness_hw_changed(led_cdev, brightness);
Don't pass TPACPI_KBD_LIGHT_AUTO_LEVEL to
led_classdev_notify_brightness_hw_changed(). Please pass 0 instead.
> +}
> +
> static struct ibm_struct kbdlight_driver_data = {
> .name = "kbdlight",
> .read = kbdlight_read,
I guess kbdlight_write() would need to call kbdlight_notify_hw_control()
to update the trigger's state as well.
Meanwhile, kbdlight_suspend() and kbdlight_resume() will override the
trigger.
> @@ -11581,22 +11665,8 @@ static bool tpacpi_driver_event(const unsigned int hkey_event)
> /* Key events are suppressed by default hotkey_user_mask */
> return false;
> case TP_HKEY_EV_KBD_LIGHT:
> - if (tp_features.kbdlight) {
> - enum led_brightness brightness;
> -
> - guard(mutex)(&kbdlight_mutex);
> -
> - /*
> - * Check the brightness actually changed, setting the brightness
> - * through kbdlight_set_level() also triggers this event.
> - */
> - brightness = kbdlight_sysfs_get(NULL);
> - if (kbdlight_brightness != brightness) {
> - kbdlight_brightness = brightness;
> - led_classdev_notify_brightness_hw_changed(
> - &tpacpi_led_kbdlight.led_classdev, brightness);
> - }
> - }
> + if (tp_features.kbdlight)
> + kbdlight_notify_change();
Please split pure refactor (moving existing code) and feature additions
into two patches.
Thanks,
Rong
> /* Key events are suppressed by default hotkey_user_mask */
> return false;
> case TP_HKEY_EV_DFR_CHANGE_ROW:
^ permalink raw reply [flat|nested] 12+ messages in thread
* [RFC PATCH v2 0/2] Add support for auto keyboard backlight
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 17:57 ` [RFC PATCH 0/1] " Rong Zhang
@ 2026-11-02 15:10 ` Qiling Zhu
2026-11-02 15:10 ` [RFC PATCH v2 1/2] platform/x86: thinkpad_acpi: Extract kbdlight_notify_change() helper Qiling Zhu
` (2 more replies)
2 siblings, 3 replies; 12+ messages in thread
From: Qiling Zhu @ 2026-11-02 15:10 UTC (permalink / raw)
To: Mark Pearson, Derek J. Clark, Rong Zhang, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar,
Qiling Zhu
Hi Mark, Rong, Ilpo,
Thanks for the review comments on v1. This v2 addresses them and is
now split into a pure refactor (1/2) and the feature addition (2/2),
as requested.
Summary of the changes:
* Kconfig: the "depends on LEDS_TRIGGERS_HW_CHANGED" is gone. While
at it I also added a "select LEDS_TRIGGERS": without it the trigger
helpers become no-op stubs and the whole feature would silently
disappear in configurations that don't opt into LEDS_TRIGGERS,
which seemed worse than a small select. HW_CHANGED support stays
optional; without it the trigger still works, only the
hardware-initiated transitions are not reported. The select goes
beyond what was suggested, so please let me know if you'd rather
drop it.
* The auto mode is never reported to the LED core as level 3: a
single helper maps it to a brightness of 0, used by both
brightness_get and the brightness_hw_changed notification, so
level 3 has no path into the LED core at all. Internal state
tracking compares raw EC levels.
* Writing a regular level through /proc/acpi/ibm/kbdlight now drops
the trigger when leaving auto mode. Instead of raising the
hardware-control notification and letting the worker handle it, I
went through the same path brightness_store() uses: drop the
trigger and hand the new level to the LED core, which applies it
after the LED_OFF queued by dropping the trigger. That way proc
and sysfs behave alike.
* suspend/resume no longer touch the LED while auto mode is active
(v1 would have restored brightness 2 there and left the trigger
state inconsistent, as Rong pointed out — thank you). The LED core
is now kept out of suspend entirely in auto mode; resume just
re-reads the EC level and reconciles. My reasoning is that in auto
mode the EC owns the backlight, so the driver should not make
decisions on its behalf — the firmware already turns the backlight
off for suspend and restores it on resume by itself. This is based
on observing my own machine (s2idle), so I may be wrong about
other models. The non-auto paths are unchanged: suspend still
turns the backlight off and resume still restores the previous
level.
One thing Sashiko (the Linux Foundation's AI review system)
brought up in its review of v1, and which I then verified in the
code, seems worth raising here: led_trigger_hw_control_changed_worker()
reads the transition flag before acquiring the LED's trigger_lock,
and the read and the transition are not atomic. If, while a worker
carrying "deactivate" is still on its way, userspace re-selects the
private trigger, the worker then drops the trigger that userspace
just set, and no further event arrives to reconcile the state.
I haven't reliably reproduced this, but the window looks real, and
the ideapad wiring seems equally exposed. My feeling is that this
would be better addressed in the trigger core than worked around in
each driver, but I may be missing something — Rong, I'd appreciate
your thoughts.
A few observations that may be useful for the discussion:
* I disassembled the DSDT and instrumented the driver to see what
MLCS returns: it is the EC status word, and bit 31 looks like an
error flag — the DSDT skips the hotkey event when it is set. On my
machine it reads 0 for all levels, with no level echo, and the
error path is not reachable. That is why activation verifies with a
read-back rather than the return value.
* The 0x1012 event is raised by the DSDT itself: MLCS issues it when
the write succeeded, the level actually changed, and the hotkey is
not masked. This explains why driver-initiated writes also see the
event and why rewriting the same level doesn't.
* The Fn+Space cycle order on this machine is 0 -> 3 -> 1 -> 2 -> 0,
confirmed by testing; sharing in case it is useful for comparison
across models. Nothing in the code or tests relies on it.
An open question from v1 that I'm still unsure about: on models
without an ALS the auto mode presumably doesn't exist — how should
the driver handle that? Mark, do you know what the EC does when
asked for level 3 on such machines, and whether there is a reliable
way to tell at probe time whether auto mode is supported? For now
the trigger is registered unconditionally on keyboard-backlight-
capable machines.
Vishnu, Rong mentioned in the v1 discussion that you are also working
on the same support using the hw control trigger framework — how is
that going? If you have answers to the questions above, or if there
is a chance to combine the two efforts, I'd be happy to coordinate.
Tested on the same ThinkPad X1 Carbon Gen 14 (linux-next
next-20261030) with targeted tests and on-hardware verification of
the affected paths, including suspend/resume; checkpatch --strict
clean.
Regards,
Qiling Zhu
This patch was developed with LLM assistance; I reviewed every design
decision against the framework sources and validated the behavior on
real hardware myself.
Qiling Zhu (2):
platform/x86: thinkpad_acpi: Extract kbdlight_notify_change() helper
platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
drivers/platform/x86/lenovo/Kconfig | 1 +
drivers/platform/x86/lenovo/thinkpad_acpi.c | 179 +++++++++++++++++---
2 files changed, 159 insertions(+), 21 deletions(-)
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [RFC PATCH v2 1/2] platform/x86: thinkpad_acpi: Extract kbdlight_notify_change() helper
2026-11-02 15:10 ` [RFC PATCH v2 0/2] " Qiling Zhu
@ 2026-11-02 15:10 ` 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 18:13 ` [RFC PATCH v2 0/2] " Rong Zhang
2 siblings, 1 reply; 12+ messages in thread
From: Qiling Zhu @ 2026-11-02 15:10 UTC (permalink / raw)
To: Mark Pearson, Derek J. Clark, Rong Zhang, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar,
Qiling Zhu
TP_HKEY_EV_KBD_LIGHT handling in tpacpi_driver_event() reads back the EC
level, updates the cached brightness and notifies the LED core inline.
Extract this block into a separate kbdlight_notify_change() helper, in
preparation for reusing it in further changes.
No functional change intended.
Signed-off-by: Qiling Zhu <kmatzkaczor244@gmail.com>
---
drivers/platform/x86/lenovo/thinkpad_acpi.c | 41 +++++++++++++--------
1 file changed, 25 insertions(+), 16 deletions(-)
diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
index 4f81855ad..3260eaa62 100644
--- a/drivers/platform/x86/lenovo/thinkpad_acpi.c
+++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c
@@ -5313,6 +5313,29 @@ static void kbdlight_resume(void)
led_classdev_resume(&tpacpi_led_kbdlight.led_classdev);
}
+/*
+ * The EC cycles the keyboard backlight level itself on Fn+Space; check
+ * the current level and update the LED core state.
+ */
+static void kbdlight_notify_change(void)
+{
+ struct led_classdev *led_cdev = &tpacpi_led_kbdlight.led_classdev;
+ enum led_brightness brightness;
+
+ guard(mutex)(&kbdlight_mutex);
+
+ /*
+ * Check the brightness actually changed, setting the brightness
+ * through kbdlight_set_level() also triggers this event.
+ */
+ brightness = kbdlight_sysfs_get(NULL);
+ if (kbdlight_brightness == brightness)
+ return;
+
+ kbdlight_brightness = brightness;
+ led_classdev_notify_brightness_hw_changed(led_cdev, brightness);
+}
+
static struct ibm_struct kbdlight_driver_data = {
.name = "kbdlight",
.read = kbdlight_read,
@@ -11581,22 +11604,8 @@ static bool tpacpi_driver_event(const unsigned int hkey_event)
/* Key events are suppressed by default hotkey_user_mask */
return false;
case TP_HKEY_EV_KBD_LIGHT:
- if (tp_features.kbdlight) {
- enum led_brightness brightness;
-
- guard(mutex)(&kbdlight_mutex);
-
- /*
- * Check the brightness actually changed, setting the brightness
- * through kbdlight_set_level() also triggers this event.
- */
- brightness = kbdlight_sysfs_get(NULL);
- if (kbdlight_brightness != brightness) {
- kbdlight_brightness = brightness;
- led_classdev_notify_brightness_hw_changed(
- &tpacpi_led_kbdlight.led_classdev, brightness);
- }
- }
+ if (tp_features.kbdlight)
+ kbdlight_notify_change();
/* Key events are suppressed by default hotkey_user_mask */
return false;
case TP_HKEY_EV_DFR_CHANGE_ROW:
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
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:10 ` Qiling Zhu
2026-11-02 15:33 ` sashiko-bot
2026-11-02 18:32 ` Rong Zhang
2026-11-02 18:13 ` [RFC PATCH v2 0/2] " Rong Zhang
2 siblings, 2 replies; 12+ messages in thread
From: Qiling Zhu @ 2026-11-02 15:10 UTC (permalink / raw)
To: Mark Pearson, Derek J. Clark, Rong Zhang, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar,
Qiling Zhu
Recent ThinkPads with an ambient light sensor (ALS) support an auto mode
for the keyboard backlight: the EC controls the backlight according to
ambient light. The mode is EC keyboard backlight level 3, above the two
fixed levels exposed so far, and is currently reachable only through the
Fn+Space shortcut, which is handled directly by the firmware.
Wire the auto mode up as a private hardware control trigger, named
"tpacpi-kbd-auto", on top of the LED hw control trigger framework:
- selecting the trigger writes level 3 (activate); the offloaded state
is derived from the current EC level
- when Fn+Space cycles into or out of auto mode, the driver notifies
the LED core about the hardware-initiated hardware control
transition, so the trigger state (shown bracketed in the trigger
sysfs attribute) follows along
- if the EC is already in auto mode at driver initialization, the
trigger is set as the default one
The auto mode is not a brightness level: max_brightness stays 2 and the
LED core is always told a brightness of 0 while in auto mode. The proc
interface keeps reporting the raw EC level, including 3, and keeps
rejecting writes of 3 with -EINVAL; writing a regular level through it
while in auto mode drops the trigger, like writing a brightness through
sysfs does. In auto mode suspend and resume are left to the EC and the
firmware; on resume the restored EC level is observed and reconciled.
LEDS_TRIGGERS is selected to keep the trigger functional in
stripped-down configurations.
Deliberately unchanged: module unload still resets the backlight to off;
retaining auto mode across unload is left for future work.
Note that Documentation/admin-guide/laptops/thinkpad-acpi.rst has no
kbdlight section at all, so there is no in-tree documentation to update;
userspace-facing semantics of hardware control triggers are documented
in Documentation/leds/leds-class.rst.
Signed-off-by: Qiling Zhu <kmatzkaczor244@gmail.com>
---
drivers/platform/x86/lenovo/Kconfig | 1 +
drivers/platform/x86/lenovo/thinkpad_acpi.c | 152 ++++++++++++++++++--
2 files changed, 141 insertions(+), 12 deletions(-)
diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index 516594993..77d159a3a 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -83,6 +83,7 @@ config THINKPAD_ACPI
select NVRAM
select NEW_LEDS
select LEDS_CLASS
+ select LEDS_TRIGGERS
select INPUT_SPARSEKMAP
help
This is a driver for the IBM and Lenovo ThinkPad laptops. It adds
diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
index 3260eaa62..7b46ad6f9 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;
+
+ /*
+ * 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;
+
+ return 0;
}
+static bool kbdlight_auto_trigger_hw_offloaded(struct led_classdev *led_cdev)
+{
+ return kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL;
+}
+
+static struct led_trigger kbdlight_auto_trigger = {
+ .name = "tpacpi-kbd-auto",
+ .trigger_type = &kbdlight_auto_trigger_type,
+ .activate = kbdlight_auto_trigger_activate,
+ .hw_offloaded = kbdlight_auto_trigger_hw_offloaded,
+};
+
static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
.led_classdev = {
.name = "tpacpi::kbd_backlight",
.max_brightness = 2,
- .flags = LED_BRIGHT_HW_CHANGED,
+ .flags = LED_BRIGHT_HW_CHANGED | LED_TRIG_HW_CHANGED,
.brightness_set_blocking = &kbdlight_sysfs_set,
.brightness_get = &kbdlight_sysfs_get,
+ .trigger_type = &kbdlight_auto_trigger_type,
}
};
@@ -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;
tp_features.kbdlight = 1;
+ /*
+ * Register the auto-mode trigger before the LED classdev: if the
+ * EC is already in auto mode, the trigger can then be set as the
+ * default one and gets activated during LED classdev registration.
+ */
+ rc = devm_led_trigger_register(&tpacpi_pdev->dev,
+ &kbdlight_auto_trigger);
+ if (rc) {
+ tp_features.kbdlight = 0;
+ return rc;
+ }
+
+ tpacpi_led_kbdlight.led_classdev.hw_control_trigger =
+ kbdlight_auto_trigger.name;
+
+ if (kbdlight_brightness == TPACPI_KBD_LIGHT_AUTO_LEVEL)
+ tpacpi_led_kbdlight.led_classdev.default_trigger =
+ kbdlight_auto_trigger.name;
+
rc = led_classdev_register(&tpacpi_pdev->dev,
&tpacpi_led_kbdlight.led_classdev);
if (rc < 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;
@@ -5293,6 +5382,8 @@ static int kbdlight_write(char *buf)
return kbdlight_set_level_and_update(level);
}
+static void kbdlight_notify_change(void);
+
static void kbdlight_suspend(void)
{
struct led_classdev *led_cdev;
@@ -5301,38 +5392,75 @@ static void kbdlight_suspend(void)
return;
led_cdev = &tpacpi_led_kbdlight.led_classdev;
+
+ /*
+ * In auto mode the EC drives the backlight and the firmware turns
+ * it off for suspend and restores it on resume by itself; keep the
+ * LED core out of it entirely.
+ */
+ if (kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL)
+ return;
+
led_update_brightness(led_cdev);
led_classdev_suspend(led_cdev);
}
static void kbdlight_resume(void)
{
+ struct led_classdev *led_cdev;
+
if (!tp_features.kbdlight)
return;
- led_classdev_resume(&tpacpi_led_kbdlight.led_classdev);
+ led_cdev = &tpacpi_led_kbdlight.led_classdev;
+
+ /* Only balance a suspend that actually suspended the LED. */
+ if (led_cdev->flags & LED_SUSPENDED) {
+ led_classdev_resume(led_cdev);
+ return;
+ }
+
+ /*
+ * Auto mode was left running over suspend; observe what the
+ * firmware actually restored.
+ */
+ kbdlight_notify_change();
}
/*
- * The EC cycles the keyboard backlight level itself on Fn+Space; check
- * the current level and update the LED core state.
+ * The EC cycles the keyboard backlight level (and auto mode) itself on
+ * Fn+Space; check the current level and update the LED core state.
*/
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);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [RFC PATCH v2 1/2] platform/x86: thinkpad_acpi: Extract kbdlight_notify_change() helper
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
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-02 15:16 UTC (permalink / raw)
To: Qiling Zhu; +Cc: linux-leds, lee
> TP_HKEY_EV_KBD_LIGHT handling in tpacpi_driver_event() reads back the EC
> level, updates the cached brightness and notifies the LED core inline.
> Extract this block into a separate kbdlight_notify_change() helper, in
> preparation for reusing it in further changes.
>
> No functional change intended.
>
> Signed-off-by: Qiling Zhu <kmatzkaczor244@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261102151211.40701-1-kmatzkaczor244@gmail.com?part=1
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
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
2026-11-02 18:32 ` Rong Zhang
1 sibling, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-02 15:33 UTC (permalink / raw)
To: Qiling Zhu; +Cc: linux-leds, lee
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
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [RFC PATCH v2 0/2] Add support for auto keyboard backlight
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:10 ` [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight Qiling Zhu
@ 2026-11-02 18:13 ` Rong Zhang
2 siblings, 0 replies; 12+ messages in thread
From: Rong Zhang @ 2026-11-02 18:13 UTC (permalink / raw)
To: Qiling Zhu, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar
Hi Qiling,
On Mon, 2026-11-02 at 23:10 +0800, Qiling Zhu wrote:
> Hi Mark, Rong, Ilpo,
>
> Thanks for the review comments on v1. This v2 addresses them and is
> now split into a pure refactor (1/2) and the feature addition (2/2),
> as requested.
You don't need to refresh the series in a hurry, unless you've got enough
reviews or may miss the cycle. Asking an LLM agent to refresh the series
is easy, but a human being needs time to review your series.
Please leave some time for others to respond.
>
> Summary of the changes:
>
> * Kconfig: the "depends on LEDS_TRIGGERS_HW_CHANGED" is gone. While
> at it I also added a "select LEDS_TRIGGERS": without it the trigger
> helpers become no-op stubs and the whole feature would silently
> disappear in configurations that don't opt into LEDS_TRIGGERS,
> which seemed worse than a small select. HW_CHANGED support stays
> optional; without it the trigger still works, only the
> hardware-initiated transitions are not reported. The select goes
> beyond what was suggested, so please let me know if you'd rather
> drop it.
>
> * The auto mode is never reported to the LED core as level 3: a
> single helper maps it to a brightness of 0, used by both
> brightness_get and the brightness_hw_changed notification, so
> level 3 has no path into the LED core at all. Internal state
> tracking compares raw EC levels.
>
> * Writing a regular level through /proc/acpi/ibm/kbdlight now drops
> the trigger when leaving auto mode. Instead of raising the
> hardware-control notification and letting the worker handle it, I
> went through the same path brightness_store() uses: drop the
> trigger and hand the new level to the LED core, which applies it
> after the LED_OFF queued by dropping the trigger. That way proc
> and sysfs behave alike.
>
> * suspend/resume no longer touch the LED while auto mode is active
> (v1 would have restored brightness 2 there and left the trigger
> state inconsistent, as Rong pointed out — thank you). The LED core
> is now kept out of suspend entirely in auto mode; resume just
> re-reads the EC level and reconciles. My reasoning is that in auto
> mode the EC owns the backlight, so the driver should not make
> decisions on its behalf — the firmware already turns the backlight
> off for suspend and restores it on resume by itself. This is based
> on observing my own machine (s2idle), so I may be wrong about
> other models. The non-auto paths are unchanged: suspend still
> turns the backlight off and resume still restores the previous
> level.
If the firmware properly handles it when hw brightness != 3, you can skip
led_classdev_{suspend,resume}() completely as long as the device supports
auto mode. They are only needed when the firmware doesn't take care of
the keyboard backlight. Skipping them should make our life mush easier.
Of course, we still need an approach to determine the support. That's why
we should leave some time for Vishnu and Mark to answer the question.
>
> One thing Sashiko (the Linux Foundation's AI review system)
> brought up in its review of v1, and which I then verified in the
> code, seems worth raising here: led_trigger_hw_control_changed_worker()
> reads the transition flag before acquiring the LED's trigger_lock,
> and the read and the transition are not atomic. If, while a worker
> carrying "deactivate" is still on its way, userspace re-selects the
> private trigger, the worker then drops the trigger that userspace
> just set, and no further event arrives to reconcile the state.
> I haven't reliably reproduced this, but the window looks real, and
> the ideapad wiring seems equally exposed. My feeling is that this
> would be better addressed in the trigger core than worked around in
> each driver, but I may be missing something — Rong, I'd appreciate
> your thoughts.
It is an unfortunate side-effect of the workqueue deferral mechanism. The
mechanism was introduced in order that:
- the LED driver can notify hw control change in an atomic context
- the LED driver can properly serialize notification handling, trigger
[de]activation, brightness_set[_blocking]() and hw_offloaded() callback
without worrying about lock reentrance
As for ideapad-laptop, it needs the second guarantee, as all said
functions or callbacks need to acquire the same mutex to prevent TOCTOU
issues.
IMHO the race condition is not a big deal. The interface was designed
with an assumption: it's the user who decides to press the shortcut
(Fn+Space) and/or select the trigger via sysfs. It can only be barely
reproduced in fuzz tests with scripts and robotic fingers, which is as
boring as fuzzing the bind/unbind interface to stress naive drivers...
That being said, I am thinking of working around this by querying
hw_offloaded() in the worker. I will find some time to prepare a patch to
see if the approach works and submit it. It won't change the interface,
so patches can be applied in an arbitrary order. Please ignore the issue
for the time being (OTOH, you don't need to rebase on top of the said
patch when it's ready).
Thanks,
Rong
>
> A few observations that may be useful for the discussion:
>
> * I disassembled the DSDT and instrumented the driver to see what
> MLCS returns: it is the EC status word, and bit 31 looks like an
> error flag — the DSDT skips the hotkey event when it is set. On my
> machine it reads 0 for all levels, with no level echo, and the
> error path is not reachable. That is why activation verifies with a
> read-back rather than the return value.
>
> * The 0x1012 event is raised by the DSDT itself: MLCS issues it when
> the write succeeded, the level actually changed, and the hotkey is
> not masked. This explains why driver-initiated writes also see the
> event and why rewriting the same level doesn't.
>
> * The Fn+Space cycle order on this machine is 0 -> 3 -> 1 -> 2 -> 0,
> confirmed by testing; sharing in case it is useful for comparison
> across models. Nothing in the code or tests relies on it.
>
> An open question from v1 that I'm still unsure about: on models
> without an ALS the auto mode presumably doesn't exist — how should
> the driver handle that? Mark, do you know what the EC does when
> asked for level 3 on such machines, and whether there is a reliable
> way to tell at probe time whether auto mode is supported? For now
> the trigger is registered unconditionally on keyboard-backlight-
> capable machines.
>
> Vishnu, Rong mentioned in the v1 discussion that you are also working
> on the same support using the hw control trigger framework — how is
> that going? If you have answers to the questions above, or if there
> is a chance to combine the two efforts, I'd be happy to coordinate.
>
> Tested on the same ThinkPad X1 Carbon Gen 14 (linux-next
> next-20261030) with targeted tests and on-hardware verification of
> the affected paths, including suspend/resume; checkpatch --strict
> clean.
>
> Regards,
> Qiling Zhu
>
> This patch was developed with LLM assistance; I reviewed every design
> decision against the framework sources and validated the behavior on
> real hardware myself.
>
> Qiling Zhu (2):
> platform/x86: thinkpad_acpi: Extract kbdlight_notify_change() helper
> platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
>
> drivers/platform/x86/lenovo/Kconfig | 1 +
> drivers/platform/x86/lenovo/thinkpad_acpi.c | 179 +++++++++++++++++---
> 2 files changed, 159 insertions(+), 21 deletions(-)
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
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
@ 2026-11-02 18:32 ` Rong Zhang
1 sibling, 0 replies; 12+ messages in thread
From: Rong Zhang @ 2026-11-02 18:32 UTC (permalink / raw)
To: Qiling Zhu, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Henrique de Moraes Holschuh
Cc: Griffin Kroah-Hartman, ibm-acpi-devel, Lee Jones, linux-kernel,
linux-leds, Pavel Machek, platform-driver-x86, vishnuocv, vsankar
Hi Qiling,
On Mon, 2026-11-02 at 23:10 +0800, Qiling Zhu wrote:
> Recent ThinkPads with an ambient light sensor (ALS) support an auto mode
> for the keyboard backlight: the EC controls the backlight according to
> ambient light. The mode is EC keyboard backlight level 3, above the two
> fixed levels exposed so far, and is currently reachable only through the
> Fn+Space shortcut, which is handled directly by the firmware.
>
> Wire the auto mode up as a private hardware control trigger, named
> "tpacpi-kbd-auto", on top of the LED hw control trigger framework:
>
> - selecting the trigger writes level 3 (activate); the offloaded state
> is derived from the current EC level
> - when Fn+Space cycles into or out of auto mode, the driver notifies
> the LED core about the hardware-initiated hardware control
> transition, so the trigger state (shown bracketed in the trigger
> sysfs attribute) follows along
> - if the EC is already in auto mode at driver initialization, the
> trigger is set as the default one
>
> The auto mode is not a brightness level: max_brightness stays 2 and the
> LED core is always told a brightness of 0 while in auto mode. The proc
> interface keeps reporting the raw EC level, including 3, and keeps
> rejecting writes of 3 with -EINVAL; writing a regular level through it
> while in auto mode drops the trigger, like writing a brightness through
> sysfs does. In auto mode suspend and resume are left to the EC and the
> firmware; on resume the restored EC level is observed and reconciled.
>
> LEDS_TRIGGERS is selected to keep the trigger functional in
> stripped-down configurations.
>
> Deliberately unchanged: module unload still resets the backlight to off;
> retaining auto mode across unload is left for future work.
>
> Note that Documentation/admin-guide/laptops/thinkpad-acpi.rst has no
> kbdlight section at all, so there is no in-tree documentation to update;
> userspace-facing semantics of hardware control triggers are documented
> in Documentation/leds/leds-class.rst.
>
> Signed-off-by: Qiling Zhu <kmatzkaczor244@gmail.com>
> ---
> drivers/platform/x86/lenovo/Kconfig | 1 +
> drivers/platform/x86/lenovo/thinkpad_acpi.c | 152 ++++++++++++++++++--
> 2 files changed, 141 insertions(+), 12 deletions(-)
>
> diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
> index 516594993..77d159a3a 100644
> --- a/drivers/platform/x86/lenovo/Kconfig
> +++ b/drivers/platform/x86/lenovo/Kconfig
> @@ -83,6 +83,7 @@ config THINKPAD_ACPI
> select NVRAM
> select NEW_LEDS
> select LEDS_CLASS
> + select LEDS_TRIGGERS
> select INPUT_SPARSEKMAP
> help
> This is a driver for the IBM and Lenovo ThinkPad laptops. It adds
> diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
> index 3260eaa62..7b46ad6f9 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;
I don't think the device needs this, as kbdlight_set_level() should be
idempotent.
> +
> + if (kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL))
> + return -EIO;
> +
> + /*
> + * 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;
Nah. Please wait until Vishnu and Mark answer about the approach to
determine the support for auto mode.
> +
> + return 0;
> }
>
> +static bool kbdlight_auto_trigger_hw_offloaded(struct led_classdev *led_cdev)
> +{
> + return kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL;
> +}
> +
> +static struct led_trigger kbdlight_auto_trigger = {
> + .name = "tpacpi-kbd-auto",
> + .trigger_type = &kbdlight_auto_trigger_type,
> + .activate = kbdlight_auto_trigger_activate,
> + .hw_offloaded = kbdlight_auto_trigger_hw_offloaded,
> +};
> +
> static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
> .led_classdev = {
> .name = "tpacpi::kbd_backlight",
> .max_brightness = 2,
> - .flags = LED_BRIGHT_HW_CHANGED,
> + .flags = LED_BRIGHT_HW_CHANGED | LED_TRIG_HW_CHANGED,
> .brightness_set_blocking = &kbdlight_sysfs_set,
> .brightness_get = &kbdlight_sysfs_get,
> + .trigger_type = &kbdlight_auto_trigger_type,
> }
> };
>
> @@ -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;
> tp_features.kbdlight = 1;
>
> + /*
> + * Register the auto-mode trigger before the LED classdev: if the
> + * EC is already in auto mode, the trigger can then be set as the
> + * default one and gets activated during LED classdev registration.
> + */
> + rc = devm_led_trigger_register(&tpacpi_pdev->dev,
> + &kbdlight_auto_trigger);
> + if (rc) {
> + tp_features.kbdlight = 0;
> + return rc;
> + }
> +
> + tpacpi_led_kbdlight.led_classdev.hw_control_trigger =
> + kbdlight_auto_trigger.name;
> +
> + if (kbdlight_brightness == TPACPI_KBD_LIGHT_AUTO_LEVEL)
> + tpacpi_led_kbdlight.led_classdev.default_trigger =
> + kbdlight_auto_trigger.name;
> +
> rc = led_classdev_register(&tpacpi_pdev->dev,
> &tpacpi_led_kbdlight.led_classdev);
> if (rc < 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)) {
You don't need this, and it causes a TOCTOU issue between
is_hw_controlled() and remove_hw_control().
> + led_trigger_remove_hw_control(led_cdev);
> + led_set_brightness(led_cdev, level);
???
Can't you simply refactor and call kbdlight_notify_change() here?
In detail:
- PATCH 1: split kbdlight_notify_change() into a function that accepts a
hw brightness level and a wrapper that queries the hardware
- PATCH 2: call the former in kbdlight_set_level_and_update(), like what
commit 07f48f668fac ("platform/x86: ideapad-laptop: add FnLock LED class
device") did
- PATCH 3: add support for auto mode
With those changes, kbdlight_set_level_and_update() should automatically
removes the triggers without the need of poking the LED core harshly.
Besides, IMO the attribute should really be deprecated with a warning
stating that writing to the attribute may desynchronize the LED core, so
that we can reduce the maintenance burden instead of infinitely working
around all sorts of such issues [1]. I am not an expert at this driver,
so I will defer it to other maintainers.
> + return 0;
> + }
> +
> + ret = kbdlight_set_level(level);
> +
> if (ret == 0 && !(led_cdev->flags & LED_SUSPENDED))
> led_cdev->brightness = level;
[1]: for example, here is a pre-existing race condition.
>
> @@ -5293,6 +5382,8 @@ static int kbdlight_write(char *buf)
> return kbdlight_set_level_and_update(level);
> }
>
> +static void kbdlight_notify_change(void);
> +
> static void kbdlight_suspend(void)
> {
> struct led_classdev *led_cdev;
> @@ -5301,38 +5392,75 @@ static void kbdlight_suspend(void)
> return;
>
> led_cdev = &tpacpi_led_kbdlight.led_classdev;
> +
> + /*
> + * In auto mode the EC drives the backlight and the firmware turns
> + * it off for suspend and restores it on resume by itself; keep the
> + * LED core out of it entirely.
> + */
> + if (kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL)
> + return;
> +
As I've said, getting rid of led_classdev_{suspend,resume}() completely
when the device supports auto mode will make our life much easier.
> led_update_brightness(led_cdev);
> led_classdev_suspend(led_cdev);
> }
>
> static void kbdlight_resume(void)
> {
> + struct led_classdev *led_cdev;
> +
> if (!tp_features.kbdlight)
> return;
>
> - led_classdev_resume(&tpacpi_led_kbdlight.led_classdev);
> + led_cdev = &tpacpi_led_kbdlight.led_classdev;
> +
> + /* Only balance a suspend that actually suspended the LED. */
> + if (led_cdev->flags & LED_SUSPENDED) {
> + led_classdev_resume(led_cdev);
> + return;
> + }
> +
> + /*
> + * Auto mode was left running over suspend; observe what the
> + * firmware actually restored.
> + */
> + kbdlight_notify_change();
If you'd like to do so, it should be in a separated patch, too.
Thanks,
Rong
> }
>
> /*
> - * The EC cycles the keyboard backlight level itself on Fn+Space; check
> - * the current level and update the LED core state.
> + * The EC cycles the keyboard backlight level (and auto mode) itself on
> + * Fn+Space; check the current level and update the LED core state.
> */
> 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);
> }
>
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-11-02 18:38 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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
2026-11-02 18:32 ` Rong Zhang
2026-11-02 18:13 ` [RFC PATCH v2 0/2] " Rong Zhang
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.