| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/07/18 17:53 | flow | patch-triage |
1mArgs:null Results: {
"EnableConfigs": [
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"__led_trigger_is_hw_controlled",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"trigger_may_offload_show",
"led_trigger_do_hw_control_transition",
"led_trigger_notify_hw_control_changed",
"__led_trigger_set",
"netdev_trig_offloaded"
],
"Reasoning": "The patch introduces functional changes to the core LED trigger subsystem, adding support for hardware-initiated trigger transitions and offload state reporting. These new code paths are reachable via sysfs attributes (`trigger_may_offload`, `brightness`, `trigger`) and driver notifications. Fuzzing should focus on the new state transition logic and offload queries.",
"WorthFuzzing": true
} |
| 1/1 | 2026/07/18 17:53 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit dde8f0b3b329354261a1554d694ed3bb1157814b\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Sat Jul 18 17:53:52 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led\nindex d4c918cc11a127..7dc95f7a3505ce 100644\n--- a/Documentation/ABI/testing/sysfs-class-led\n+++ b/Documentation/ABI/testing/sysfs-class-led\n@@ -78,6 +78,31 @@ Description:\n \t\t(which would often be configured in the device tree for the\n \t\thardware).\n \n+What:\t\t/sys/class/leds/\u003cled\u003e/trigger_may_offload\n+Date:\t\tJuly 2026\n+KernelVersion:\t7.3\n+Contact:\tlinux-leds@vger.kernel.org\n+Description:\n+\t\tNames and states of triggers that may be offloaded to hardware.\n+\t\tSuch triggers are also called \"hardware control trigger\" in some\n+\t\tcontext.\n+\n+\t\tOnly exists when the LED supports trigger offload.\n+\n+\t\tReading this file returns a list of triggers that are capable to\n+\t\tbe offloaded. The optional brackets around the trigger name\n+\t\tindicate the state of the current trigger:\n+\n+\t\t- `foo_trigger`: the trigger is not selected.\n+\t\t- `\u003cfoo_trigger\u003e`: the trigger is selected, but falls back to\n+\t\t software blink for some reason (e.g., incompatible trigger\n+\t\t parameters)\n+\t\t- `[foo_trigger]`: the trigger is selected and offloaded to\n+\t\t hardware.\n+\n+\t\tThe \"netdev\" trigger also provides a custom attribute to\n+\t\tindicate its state, see `/sys/class/leds/\u003cled\u003e/offloaded`.\n+\n What:\t\t/sys/class/leds/\u003cled\u003e/inverted\n Date:\t\tJanuary 2011\n KernelVersion:\t2.6.38\ndiff --git a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev\nindex ed46b37ab8a284..a5146ea1e3e61c 100644\n--- a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev\n+++ b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev\n@@ -75,6 +75,9 @@ Description:\n \t\tIf 1, the LED blinking in requested mode is offloaded to\n \t\thardware.\n \n+\t\tLED trigger core also provides a generic attribute for this\n+\t\tpurpose, see `/sys/class/leds/\u003cled\u003e/trigger_may_offload`.\n+\n What:\t\t/sys/class/leds/\u003cled\u003e/link_10\n Date:\t\tJun 2023\n KernelVersion:\t6.5\ndiff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst\nindex 3913966cfdac04..adbc57b9f49c33 100644\n--- a/Documentation/leds/leds-class.rst\n+++ b/Documentation/leds/leds-class.rst\n@@ -242,6 +242,9 @@ ops and needs to declare specific support for the supported triggers.\n \n With hw control we refer to the LED driven by hardware.\n \n+A sysfs attribute `trigger_may_offload` is provided for userspace to\n+query supported triggers and their states.\n+\n LED driver must define the following value to support hw control:\n \n - hw_control_trigger:\n@@ -298,6 +301,15 @@ LED driver must implement the following API to support hw control:\n Returns a pointer to a struct device or NULL if nothing\n is currently attached.\n \n+LED trigger should implement the following API to indicate hw control:\n+ - offloaded:\n+ return a boolean indicating if the trigger is currently\n+ offloaded to hardware.\n+\n+ If a trigger doesn't implement this callback, the default\n+ value will be true for private triggers and false for generic\n+ ones.\n+\n LED driver can activate additional modes by default to workaround the\n impossibility of supporting each different mode on the supported trigger.\n Examples are hardcoding the blink speed to a set interval, enable special\n@@ -311,9 +323,69 @@ the end use hw_control_set to activate hw control.\n A trigger can use hw_control_get to check if a LED is already in hw control\n and init their flags.\n \n+Alternatively, a private trigger can be implemented along with the LED driver if\n+the LED's hardware control doesn't fit any generic trigger. To associate the\n+private trigger with the LED classdev, their `trigger_type` must be the same. To\n+declare that the private trigger provides hardware control for the associated\n+LED classdev, set the `hw_control_trigger` string to the trigger's name. Since\n+both the LED classdev and the private trigger are in the same LED driver, it's\n+not necessary for them to coordinate via `hw_control_*` callbacks.\n+\n When the LED is in hw control, no software blink is possible and doing so\n will effectively disable hw control.\n \n+Hardware-initiated trigger transition\n+=====================================\n+\n+Some hardware can autonomously activate/deactivate hardware control. After that,\n+the LED hardware notifies the LED driver.\n+\n+If the driver can detect such transitions and thus wants to notify the LED core\n+to update the current trigger then the `LED_TRIG_HW_CHANGED` flag must be set in\n+flags before registering. To update the current trigger accordingly, call\n+`led_trigger_notify_hw_control_changed` on the LED classdev.\n+\n+This capability is restricted to the LED device's private trigger. The private\n+trigger must have been properly registered (see above) and named after\n+`hw_control_trigger`.\n+\n+Only two transitions are defined:\n+\n+- \"none\" =\u003e private trigger:\n+ This happens when the hardware autonomously activates hardware control\n+ and when \"none\" (i.e., no trigger) is currently active. If the private\n+ trigger is already active when the method is called, this is essentially\n+ a no-op.\n+\n+ The activation sequence for the private trigger will be executed as\n+ normal.\n+\n+ The LED driver and its private trigger must be able to handle the\n+ activation sequence even if the hardware is currently in hardware\n+ control.\n+\n+ If error occurs in the activation sequence, the LED Trigger core reverts\n+ the effective trigger to \"none\".\n+\n+- private trigger =\u003e \"none\"\n+ This happens when the hardware autonomously deactivates hardware control\n+ and when the private trigger is currently active. If \"none\" (i.e., no\n+ trigger) is active when the method is called, this is essentially a\n+ no-op.\n+\n+ The deactivation sequence for the private trigger will be executed as\n+ normal, except that the current LED brightness is retained. The reason\n+ for keeping the brightness unchanged is that some hardware may choose a\n+ specific brightness instead of simply turning off the LED after\n+ autonomously deactivating hardware control.\n+\n+ The LED driver and its private trigger must be able to handle the\n+ deactivation sequence even if the hardware is not currently in hardware\n+ control.\n+\n+If the current trigger is neither the private trigger nor \"none\", no transition\n+will be made.\n+\n Known Issues\n ============\n \ndiff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c\nindex a51b0ed5388644..2460fcf0c4690a 100644\n--- a/drivers/leds/led-class.c\n+++ b/drivers/leds/led-class.c\n@@ -27,16 +27,6 @@ static LIST_HEAD(leds_lookup_list);\n \n static struct workqueue_struct *leds_wq;\n \n-static bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev)\n-{\n-#ifdef CONFIG_LEDS_TRIGGERS\n-\tguard(rwsem_read)(\u0026led_cdev-\u003etrigger_lock);\n-\treturn led_cdev-\u003etrigger \u0026\u0026 led_cdev-\u003etrigger-\u003etrigger_type;\n-#else\n-\treturn false;\n-#endif\n-}\n-\n static ssize_t brightness_show(struct device *dev,\n \t\tstruct device_attribute *attr, char *buf)\n {\n@@ -74,6 +64,9 @@ static ssize_t brightness_store(struct device *dev,\n \n \tif (state == LED_OFF)\n \t\tled_trigger_remove(led_cdev);\n+\telse\n+\t\tled_trigger_remove_hw_control(led_cdev);\n+\n \tled_set_brightness(led_cdev, state);\n \n \tret = size;\n@@ -103,8 +96,30 @@ static const struct bin_attribute *const led_trigger_bin_attrs[] = {\n \t\u0026bin_attr_trigger,\n \tNULL,\n };\n+\n+static DEVICE_ATTR_RO(trigger_may_offload);\n+static struct attribute *led_trigger_attrs[] = {\n+\t\u0026dev_attr_trigger_may_offload.attr,\n+\tNULL\n+};\n+\n+static umode_t led_trigger_is_visible(struct kobject *kobj,\n+\t\t\t\t struct attribute *attr,\n+\t\t\t\t int idx)\n+{\n+\tstruct device *dev = kobj_to_dev(kobj);\n+\tstruct led_classdev *led_cdev = dev_get_drvdata(dev);\n+\n+\tif (attr == \u0026dev_attr_trigger_may_offload.attr)\n+\t\treturn led_cdev-\u003ehw_control_trigger ? attr-\u003emode : 0;\n+\n+\treturn attr-\u003emode;\n+}\n+\n static const struct attribute_group led_trigger_group = {\n \t.bin_attrs = led_trigger_bin_attrs,\n+\t.attrs = led_trigger_attrs,\n+\t.is_visible = led_trigger_is_visible,\n };\n #endif\n \ndiff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c\nindex b1223218bda113..6ae28cbd1c7783 100644\n--- a/drivers/leds/led-triggers.c\n+++ b/drivers/leds/led-triggers.c\n@@ -7,9 +7,12 @@\n * Author: Richard Purdie \u003crpurdie@openedhand.com\u003e\n */\n \n+#include \u003clinux/bug.h\u003e\n+#include \u003clinux/cleanup.h\u003e\n #include \u003clinux/export.h\u003e\n #include \u003clinux/kernel.h\u003e\n #include \u003clinux/list.h\u003e\n+#include \u003clinux/lockdep.h\u003e\n #include \u003clinux/spinlock.h\u003e\n #include \u003clinux/device.h\u003e\n #include \u003clinux/timer.h\u003e\n@@ -33,6 +36,33 @@ trigger_relevant(struct led_classdev *led_cdev, struct led_trigger *trig)\n \treturn !trig-\u003etrigger_type || trig-\u003etrigger_type == led_cdev-\u003etrigger_type;\n }\n \n+static bool __led_trigger_is_hw_controlled(struct led_classdev *led_cdev)\n+{\n+\tlockdep_assert_held(\u0026led_cdev-\u003etrigger_lock);\n+\n+\tif (!led_cdev-\u003etrigger)\n+\t\treturn false;\n+\n+\tif (!led_cdev-\u003ehw_control_trigger ||\n+\t strcmp(led_cdev-\u003ehw_control_trigger, led_cdev-\u003etrigger-\u003ename))\n+\t\treturn false;\n+\n+\tif (led_cdev-\u003etrigger-\u003eoffloaded)\n+\t\treturn led_cdev-\u003etrigger-\u003eoffloaded(led_cdev);\n+\n+\tdev_warn_once(led_cdev-\u003edev, \"hw control trigger %s doesn't implement offloaded()\\n\",\n+\t\t led_cdev-\u003etrigger-\u003ename);\n+\n+\treturn led_cdev-\u003etrigger-\u003etrigger_type;\n+}\n+\n+bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev)\n+{\n+\tguard(rwsem_read)(\u0026led_cdev-\u003etrigger_lock);\n+\treturn __led_trigger_is_hw_controlled(led_cdev);\n+}\n+EXPORT_SYMBOL_GPL(led_trigger_is_hw_controlled);\n+\n ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,\n \t\t\t const struct bin_attribute *bin_attr, char *buf,\n \t\t\t loff_t pos, size_t count)\n@@ -163,7 +193,8 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,\n EXPORT_SYMBOL_GPL(led_trigger_read);\n \n /* Caller must ensure led_cdev-\u003etrigger_lock held */\n-int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)\n+static int __led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig,\n+\t\t\t bool hw_triggered)\n {\n \tchar *event = NULL;\n \tchar *envp[2];\n@@ -194,7 +225,21 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)\n \t\tled_cdev-\u003etrigger_data = NULL;\n \t\tled_cdev-\u003eactivated = false;\n \t\tled_cdev-\u003eflags \u0026= ~LED_INIT_DEFAULT_TRIGGER;\n-\t\tled_set_brightness(led_cdev, LED_OFF);\n+\n+\t\t/*\n+\t\t * Hardware may have selected a new brightness level during its\n+\t\t * hardware control transition, so only reset brightness if we\n+\t\t * are switching to another trigger or if the switching is not\n+\t\t * hardware triggered.\n+\t\t *\n+\t\t * Note that this does not apply to the error path, as running\n+\t\t * into the error path implies a none =\u003e private trigger\n+\t\t * transition. This hints that the LED driver and its private\n+\t\t * trigger must have some fundamental bugs, so don't bother\n+\t\t * leaving the LED in an undefined state.\n+\t\t */\n+\t\tif (trig || !hw_triggered)\n+\t\t\tled_set_brightness(led_cdev, LED_OFF);\n \t}\n \tif (trig) {\n \t\tspin_lock(\u0026trig-\u003eleddev_list_lock);\n@@ -258,6 +303,11 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)\n \n \treturn ret;\n }\n+\n+int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)\n+{\n+\treturn __led_trigger_set(led_cdev, trig, false);\n+}\n EXPORT_SYMBOL_GPL(led_trigger_set);\n \n void led_trigger_remove(struct led_classdev *led_cdev)\n@@ -268,6 +318,15 @@ void led_trigger_remove(struct led_classdev *led_cdev)\n }\n EXPORT_SYMBOL_GPL(led_trigger_remove);\n \n+void led_trigger_remove_hw_control(struct led_classdev *led_cdev)\n+{\n+\tguard(rwsem_write)(\u0026led_cdev-\u003etrigger_lock);\n+\n+\tif (__led_trigger_is_hw_controlled(led_cdev))\n+\t\tled_trigger_set(led_cdev, NULL);\n+}\n+EXPORT_SYMBOL_GPL(led_trigger_remove_hw_control);\n+\n static bool led_match_default_trigger(struct led_classdev *led_cdev,\n \t\t\t\t struct led_trigger *trig)\n {\n@@ -313,6 +372,32 @@ void led_trigger_set_default(struct led_classdev *led_cdev)\n }\n EXPORT_SYMBOL_GPL(led_trigger_set_default);\n \n+ssize_t trigger_may_offload_show(struct device *dev,\n+\t\t\t\t struct device_attribute *attr, char *buf)\n+{\n+\tstruct led_classdev *led_cdev = dev_get_drvdata(dev);\n+\tstruct led_trigger *trig;\n+\tbool hit, offloaded;\n+\tint len;\n+\n+\tguard(mutex)(\u0026led_cdev-\u003eled_access);\n+\tguard(rwsem_read)(\u0026led_cdev-\u003etrigger_lock);\n+\n+\ttrig = led_cdev-\u003etrigger;\n+\n+\toffloaded = __led_trigger_is_hw_controlled(led_cdev);\n+\thit = offloaded || (trig \u0026\u0026 !strcmp(led_cdev-\u003ehw_control_trigger, trig-\u003ename));\n+\n+\t/* [offloaded] \u003cactive_but_not_offloaded\u003e inactive */\n+\tlen = sysfs_emit(buf, \"%s%s%s\\n\",\n+\t\t\t offloaded ? \"[\" : (hit ? \"\u003c\" : \"\"),\n+\t\t\t led_cdev-\u003ehw_control_trigger,\n+\t\t\t offloaded ? \"]\" : (hit ? \"\u003e\" : \"\"));\n+\n+\treturn len;\n+}\n+EXPORT_SYMBOL_GPL(trigger_may_offload_show);\n+\n /* LED Trigger Interface */\n \n int led_trigger_register(struct led_trigger *trig)\n@@ -403,6 +488,63 @@ int devm_led_trigger_register(struct device *dev,\n }\n EXPORT_SYMBOL_GPL(devm_led_trigger_register);\n \n+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\n+static void led_trigger_do_hw_control_transition(struct led_classdev *led_cdev, bool activate,\n+\t\t\t\t\t\t struct led_trigger *hc_trig)\n+{\n+\tint err = 0;\n+\n+\tif (!led_cdev-\u003etrigger) {\n+\t\t/* \"none\" =\u003e private trigger. */\n+\t\tif (activate)\n+\t\t\terr = __led_trigger_set(led_cdev, hc_trig, true);\n+\t} else if (led_cdev-\u003etrigger == hc_trig) {\n+\t\t/* private trigger =\u003e \"none\". */\n+\t\tif (!activate)\n+\t\t\terr = __led_trigger_set(led_cdev, NULL, true);\n+\t} else {\n+\t\t/* Other trigger is active. */\n+\t\tdev_dbg(led_cdev-\u003edev,\n+\t\t\t\"Ignoring hw control transition (%s %s) while %s is active\",\n+\t\t\tactivate ? \"activate\" : \"deactivate\", hc_trig-\u003ename,\n+\t\t\tled_cdev-\u003etrigger-\u003ename);\n+\n+\t\treturn;\n+\t}\n+\n+\tif (err)\n+\t\tdev_warn(led_cdev-\u003edev, \"Failed to %s %s in hw control transition: %d\",\n+\t\t\t activate ? \"activate\" : \"deactivate\", hc_trig-\u003ename, err);\n+}\n+\n+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)\n+{\n+\tstruct led_trigger *trig;\n+\n+\t/* Restricted to private triggers. */\n+\tif (WARN_ON(!(led_cdev-\u003eflags \u0026 LED_TRIG_HW_CHANGED) ||\n+\t\t !led_cdev-\u003ehw_control_trigger || !led_cdev-\u003etrigger_type))\n+\t\treturn;\n+\n+\tscoped_guard(rwsem_read, \u0026triggers_list_lock) {\n+\t\tlist_for_each_entry(trig, \u0026trigger_list, next_trig) {\n+\t\t\tif (trig-\u003etrigger_type == led_cdev-\u003etrigger_type \u0026\u0026\n+\t\t\t !strcmp(trig-\u003ename, led_cdev-\u003ehw_control_trigger)) {\n+\t\t\t\tguard(rwsem_write)(\u0026led_cdev-\u003etrigger_lock);\n+\n+\t\t\t\tled_trigger_do_hw_control_transition(led_cdev, activate, trig);\n+\t\t\t\treturn;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tdev_err(led_cdev-\u003edev,\n+\t\t\"%s() is called, but the private trigger (%s) is not properly registered\\n\",\n+\t\t__func__, led_cdev-\u003ehw_control_trigger);\n+}\n+EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed);\n+#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */\n+\n /* Simple LED Trigger Interface */\n \n void led_trigger_event(struct led_trigger *trig,\ndiff --git a/drivers/leds/leds-cros_ec.c b/drivers/leds/leds-cros_ec.c\nindex 1844d0cd5f5284..6db83d0152775f 100644\n--- a/drivers/leds/leds-cros_ec.c\n+++ b/drivers/leds/leds-cros_ec.c\n@@ -85,12 +85,18 @@ static int cros_ec_led_trigger_activate(struct led_classdev *led_cdev)\n \treturn cros_ec_led_send_cmd(priv-\u003ecros_ec, \u0026arg);\n }\n \n+static bool cros_ec_led_trigger_offloaded(struct led_classdev *led_cdev)\n+{\n+\treturn true;\n+}\n+\n static struct led_hw_trigger_type cros_ec_led_trigger_type;\n \n static struct led_trigger cros_ec_led_trigger = {\n \t.name = \"chromeos-auto\",\n \t.trigger_type = \u0026cros_ec_led_trigger_type,\n \t.activate = cros_ec_led_trigger_activate,\n+\t.offloaded = cros_ec_led_trigger_offloaded,\n };\n \n static int cros_ec_led_brightness_set_blocking(struct led_classdev *led_cdev,\ndiff --git a/drivers/leds/leds-turris-omnia.c b/drivers/leds/leds-turris-omnia.c\nindex ed6a47bbb44f9c..32d40d176d3f11 100644\n--- a/drivers/leds/leds-turris-omnia.c\n+++ b/drivers/leds/leds-turris-omnia.c\n@@ -195,10 +195,16 @@ static void omnia_hwtrig_deactivate(struct led_classdev *cdev)\n \t\t\terr);\n }\n \n+static bool omnia_hwtrig_offloaded(struct led_classdev *cdev)\n+{\n+\treturn true;\n+}\n+\n static struct led_trigger omnia_hw_trigger = {\n \t.name\t\t= \"omnia-mcu\",\n \t.activate\t= omnia_hwtrig_activate,\n \t.deactivate\t= omnia_hwtrig_deactivate,\n+\t.offloaded\t= omnia_hwtrig_offloaded,\n \t.trigger_type\t= \u0026omnia_hw_trigger_type,\n };\n \n@@ -251,6 +257,7 @@ static int omnia_led_register(struct i2c_client *client, struct omnia_led *led,\n \t * by LED class from the linux,default-trigger property.\n \t */\n \tcdev-\u003edefault_trigger = omnia_hw_trigger.name;\n+\tcdev-\u003ehw_control_trigger = omnia_hw_trigger.name;\n \n \t/* Put the LED into software mode */\n \tret = omnia_cmd_write_u8(client, OMNIA_CMD_LED_MODE, OMNIA_CMD_LED_MODE_LED(led-\u003ereg) |\ndiff --git a/drivers/leds/leds.h b/drivers/leds/leds.h\nindex bee46651e068f7..b08a289397e463 100644\n--- a/drivers/leds/leds.h\n+++ b/drivers/leds/leds.h\n@@ -27,6 +27,8 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,\n ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,\n \t\t\tconst struct bin_attribute *bin_attr, char *buf,\n \t\t\tloff_t pos, size_t count);\n+ssize_t trigger_may_offload_show(struct device *dev,\n+\t\t\t\t struct device_attribute *attr, char *buf);\n \n extern struct rw_semaphore leds_list_lock;\n extern struct list_head leds_list;\ndiff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig\nindex c11282a74b5ac3..798122154049b4 100644\n--- a/drivers/leds/trigger/Kconfig\n+++ b/drivers/leds/trigger/Kconfig\n@@ -9,6 +9,15 @@ menuconfig LEDS_TRIGGERS\n \n if LEDS_TRIGGERS\n \n+config LEDS_TRIGGERS_HW_CHANGED\n+\tbool \"LED hardware-initiated trigger transition support\"\n+\thelp\n+\t This option enables support for hardware initiated hardware control\n+\t transitions, where the LED hardware autonomously switches between\n+\t \"none\" (i.e., no trigger) and its private trigger.\n+\n+\t See Documentation/leds/leds-class.rst for details.\n+\n config LEDS_TRIGGER_TIMER\n \ttristate \"LED Timer Trigger\"\n \thelp\ndiff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c\nindex 64c078e997f28a..a26109ca4b1c9f 100644\n--- a/drivers/leds/trigger/ledtrig-netdev.c\n+++ b/drivers/leds/trigger/ledtrig-netdev.c\n@@ -754,10 +754,18 @@ static void netdev_trig_deactivate(struct led_classdev *led_cdev)\n \tkfree(trigger_data);\n }\n \n+static bool netdev_trig_offloaded(struct led_classdev *led_cdev)\n+{\n+\tstruct led_netdev_data *trigger_data = led_get_trigger_data(led_cdev);\n+\n+\treturn trigger_data-\u003ehw_control;\n+}\n+\n static struct led_trigger netdev_led_trigger = {\n \t.name = \"netdev\",\n \t.activate = netdev_trig_activate,\n \t.deactivate = netdev_trig_deactivate,\n+\t.offloaded = netdev_trig_offloaded,\n \t.groups = netdev_trig_groups,\n };\n \ndiff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig\nindex 4443f40ef8aa7e..e92b1e90079510 100644\n--- a/drivers/platform/x86/lenovo/Kconfig\n+++ b/drivers/platform/x86/lenovo/Kconfig\n@@ -16,6 +16,7 @@ config IDEAPAD_LAPTOP\n \tselect INPUT_SPARSEKMAP\n \tselect NEW_LEDS\n \tselect LEDS_CLASS\n+\tselect LEDS_TRIGGERS\n \thelp\n \t This is a driver for Lenovo IdeaPad netbooks contains drivers for\n \t rfkill switch, hotkey, fan control and backlight control.\ndiff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c\nindex 4fbc904f1fc390..253d2962b92762 100644\n--- a/drivers/platform/x86/lenovo/ideapad-laptop.c\n+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c\n@@ -9,6 +9,7 @@\n #define pr_fmt(fmt) KBUILD_MODNAME \": \" fmt\n \n #include \u003clinux/acpi.h\u003e\n+#include \u003clinux/atomic.h\u003e\n #include \u003clinux/backlight.h\u003e\n #include \u003clinux/bitfield.h\u003e\n #include \u003clinux/bitops.h\u003e\n@@ -25,7 +26,9 @@\n #include \u003clinux/jiffies.h\u003e\n #include \u003clinux/kernel.h\u003e\n #include \u003clinux/leds.h\u003e\n+#include \u003clinux/lockdep.h\u003e\n #include \u003clinux/module.h\u003e\n+#include \u003clinux/mutex.h\u003e\n #include \u003clinux/platform_device.h\u003e\n #include \u003clinux/platform_profile.h\u003e\n #include \u003clinux/power_supply.h\u003e\n@@ -134,10 +137,31 @@ enum {\n };\n \n /*\n- * These correspond to the number of supported states - 1\n- * Future keyboard types may need a new system, if there's a collision\n- * KBD_BL_TRISTATE_AUTO has no way to report or set the auto state\n- * so it effectively has 3 states, but needs to handle 4\n+ * The enumeration has two purposes:\n+ * - as an internal identifier for all known types of keyboard backlight\n+ * - as a mandatory parameter of the KBLC command\n+ *\n+ * For each type, the hardware brightness values are defined as follows:\n+ * +--------------------------+----------+-----+------+------+\n+ * | Hardware brightness | 0 | 1 | 2 | 3 |\n+ * | Type | | | | |\n+ * +--------------------------+----------+-----+------+------+\n+ * | KBD_BL_STANDARD | off | on | N/A | N/A |\n+ * +--------------------------+----------+-----+------+------+\n+ * | KBD_BL_TRISTATE | off | low | high | N/A |\n+ * +--------------------------+----------+-----+------+------+\n+ * | KBD_BL_TRISTATE_AUTO | off | low | high | auto |\n+ * +--------------------------+----------+-----+------+------+\n+ *\n+ * We map LED classdev brightness for KBD_BL_TRISTATE_AUTO as follows:\n+ * +--------------------------+----------+-----+------+\n+ * | LED classdev brightness | 0 | 1 | 2 |\n+ * | Operation | | | |\n+ * +--------------------------+----------+-----+------+\n+ * | Read | off/auto | low | high |\n+ * +--------------------------+----------+-----+------+\n+ * | Write | off | low | high |\n+ * +--------------------------+----------+-----+------+\n */\n enum {\n \tKBD_BL_STANDARD = 1,\n@@ -145,6 +169,8 @@ enum {\n \tKBD_BL_TRISTATE_AUTO = 3,\n };\n \n+#define KBD_BL_AUTO_MODE_HW_BRIGHTNESS\t3\n+\n #define KBD_BL_QUERY_TYPE\t\t0x1\n #define KBD_BL_TRISTATE_TYPE\t\t0x5\n #define KBD_BL_TRISTATE_AUTO_TYPE\t0x7\n@@ -203,7 +229,9 @@ struct ideapad_private {\n \t\tbool initialized;\n \t\tint type;\n \t\tstruct led_classdev led;\n-\t\tunsigned int last_brightness;\n+\t\tatomic_t last_hw_brightness;\n+\n+\t\tstruct mutex notif_mutex; /* protects notifications */\n \t} kbd_bl;\n \tstruct {\n \t\tbool initialized;\n@@ -1592,7 +1620,24 @@ static int ideapad_kbd_bl_check_tristate(int type)\n \treturn (type == KBD_BL_TRISTATE) || (type == KBD_BL_TRISTATE_AUTO);\n }\n \n-static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)\n+static int ideapad_kbd_bl_brightness_parse(struct ideapad_private *priv, int hw_brightness)\n+{\n+\t/* Off, low or high */\n+\tif (hw_brightness \u003c= priv-\u003ekbd_bl.led.max_brightness)\n+\t\treturn hw_brightness;\n+\n+\t/* Auto (controlled by EC according to ALS), report as off */\n+\tif (priv-\u003ekbd_bl.type == KBD_BL_TRISTATE_AUTO \u0026\u0026\n+\t hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS)\n+\t\treturn 0;\n+\n+\t/* Unknown value */\n+\tdev_warn(\u0026priv-\u003eplatform_device-\u003edev,\n+\t\t \"Unknown keyboard backlight value: %d\", hw_brightness);\n+\treturn -EINVAL;\n+}\n+\n+static int ideapad_kbd_bl_hw_brightness_get(struct ideapad_private *priv)\n {\n \tunsigned long value;\n \tint err;\n@@ -1606,21 +1651,7 @@ static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)\n \t\tif (err)\n \t\t\treturn err;\n \n-\t\t/* Convert returned value to brightness level */\n-\t\tvalue = FIELD_GET(KBD_BL_GET_BRIGHTNESS, value);\n-\n-\t\t/* Off, low or high */\n-\t\tif (value \u003c= priv-\u003ekbd_bl.led.max_brightness)\n-\t\t\treturn value;\n-\n-\t\t/* Auto, report as off */\n-\t\tif (value == priv-\u003ekbd_bl.led.max_brightness + 1)\n-\t\t\treturn 0;\n-\n-\t\t/* Unknown value */\n-\t\tdev_warn(\u0026priv-\u003eplatform_device-\u003edev,\n-\t\t\t \"Unknown keyboard backlight value: %lu\", value);\n-\t\treturn -EINVAL;\n+\t\treturn FIELD_GET(KBD_BL_GET_BRIGHTNESS, value);\n \t}\n \n \terr = eval_hals(priv-\u003eadev-\u003ehandle, \u0026value);\n@@ -1630,6 +1661,16 @@ static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)\n \treturn !!test_bit(HALS_KBD_BL_STATE_BIT, \u0026value);\n }\n \n+static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)\n+{\n+\tint hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);\n+\n+\tif (hw_brightness \u003c 0)\n+\t\treturn hw_brightness;\n+\n+\treturn ideapad_kbd_bl_brightness_parse(priv, hw_brightness);\n+}\n+\n static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_classdev *led_cdev)\n {\n \tstruct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);\n@@ -1637,62 +1678,123 @@ static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_cla\n \treturn ideapad_kbd_bl_brightness_get(priv);\n }\n \n-static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned int brightness)\n+static int ideapad_kbd_bl_hw_brightness_set(struct ideapad_private *priv, int hw_brightness)\n {\n-\tint err;\n \tunsigned long value;\n \tint type = priv-\u003ekbd_bl.type;\n+\tint err;\n \n \tif (ideapad_kbd_bl_check_tristate(type)) {\n-\t\tif (brightness \u003e priv-\u003ekbd_bl.led.max_brightness)\n-\t\t\treturn -EINVAL;\n-\n-\t\tvalue = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) |\n+\t\tvalue = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, hw_brightness) |\n \t\t\tFIELD_PREP(KBD_BL_COMMAND_TYPE, type) |\n \t\t\tKBD_BL_COMMAND_SET;\n \t\terr = exec_kblc(priv-\u003eadev-\u003ehandle, value);\n \t} else {\n-\t\terr = exec_sals(priv-\u003eadev-\u003ehandle, brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF);\n+\t\tvalue = hw_brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF;\n+\t\terr = exec_sals(priv-\u003eadev-\u003ehandle, value);\n \t}\n-\n \tif (err)\n \t\treturn err;\n \n-\tpriv-\u003ekbd_bl.last_brightness = brightness;\n+\tatomic_set(\u0026priv-\u003ekbd_bl.last_hw_brightness, hw_brightness);\n \n \treturn 0;\n }\n \n+static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, int brightness)\n+{\n+\tif (brightness \u003e priv-\u003ekbd_bl.led.max_brightness)\n+\t\treturn -EINVAL;\n+\n+\treturn ideapad_kbd_bl_hw_brightness_set(priv, brightness);\n+}\n+\n static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev,\n \t\t\t\t\t\t enum led_brightness brightness)\n {\n \tstruct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);\n \n+\t/*\n+\t * When deinitializing: It must be the side effect of led_cdev\n+\t * unregistration when our private trigger is active. We've set\n+\t * LED_RETAIN_AT_SHUTDOWN to retain led_cdev brightness level.\n+\t * To do the same for auto mode, gate changes and return early.\n+\t */\n+\tif (unlikely(!priv-\u003ekbd_bl.initialized))\n+\t\treturn 0;\n+\n \treturn ideapad_kbd_bl_brightness_set(priv, brightness);\n }\n \n+static bool ideapad_kbd_bl_auto_trigger_offloaded(struct led_classdev *led_cdev)\n+{\n+\tstruct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);\n+\n+\treturn atomic_read(\u0026priv-\u003ekbd_bl.last_hw_brightness) == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;\n+}\n+\n+static int ideapad_kbd_bl_auto_trigger_activate(struct led_classdev *led_cdev)\n+{\n+\tstruct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);\n+\n+\treturn ideapad_kbd_bl_hw_brightness_set(priv, KBD_BL_AUTO_MODE_HW_BRIGHTNESS);\n+}\n+\n+static struct led_hw_trigger_type ideapad_kbd_bl_auto_trigger_type;\n+\n+static struct led_trigger ideapad_kbd_bl_auto_trigger = {\n+\t.name = \"ideapad-auto\",\n+\t.trigger_type = \u0026ideapad_kbd_bl_auto_trigger_type,\n+\t.activate = ideapad_kbd_bl_auto_trigger_activate,\n+\t.offloaded = ideapad_kbd_bl_auto_trigger_offloaded,\n+};\n+\n+static bool ideapad_kbd_bl_auto_trigger_registered;\n+\n+static void ideapad_kbd_bl_notify_hw_control(struct ideapad_private *priv,\n+\t\t\t\t\t int hw_brightness, int last_hw_brightness)\n+{\n+\tbool hw_control, last_hw_control;\n+\n+\tif (priv-\u003ekbd_bl.type != KBD_BL_TRISTATE_AUTO)\n+\t\treturn;\n+\n+\thw_control = hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;\n+\tlast_hw_control = last_hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;\n+\n+\tif (hw_control != last_hw_control)\n+\t\tled_trigger_notify_hw_control_changed(\u0026priv-\u003ekbd_bl.led, hw_control);\n+}\n+\n static void ideapad_kbd_bl_notify(struct ideapad_private *priv)\n {\n-\tint brightness;\n+\tint hw_brightness, brightness, last_hw_brightness;\n \n \tif (!priv-\u003ekbd_bl.initialized)\n \t\treturn;\n \n-\tbrightness = ideapad_kbd_bl_brightness_get(priv);\n-\tif (brightness \u003c 0)\n+\tguard(mutex)(\u0026priv-\u003ekbd_bl.notif_mutex);\n+\n+\thw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);\n+\tif (hw_brightness \u003c 0)\n \t\treturn;\n \n-\tif (brightness == priv-\u003ekbd_bl.last_brightness)\n+\tbrightness = ideapad_kbd_bl_brightness_parse(priv, hw_brightness);\n+\tif (brightness \u003c 0)\n+\t\treturn; /* Reject insane values early. */\n+\n+\tlast_hw_brightness = atomic_xchg(\u0026priv-\u003ekbd_bl.last_hw_brightness, hw_brightness);\n+\tif (hw_brightness == last_hw_brightness)\n \t\treturn;\n \n-\tpriv-\u003ekbd_bl.last_brightness = brightness;\n+\tideapad_kbd_bl_notify_hw_control(priv, hw_brightness, last_hw_brightness);\n \n \tled_classdev_notify_brightness_hw_changed(\u0026priv-\u003ekbd_bl.led, brightness);\n }\n \n static int ideapad_kbd_bl_init(struct ideapad_private *priv)\n {\n-\tint brightness, err;\n+\tint hw_brightness, err;\n \n \tif (!priv-\u003efeatures.kbd_bl)\n \t\treturn -ENODEV;\n@@ -1700,28 +1802,68 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)\n \tif (WARN_ON(priv-\u003ekbd_bl.initialized))\n \t\treturn -EEXIST;\n \n-\tif (ideapad_kbd_bl_check_tristate(priv-\u003ekbd_bl.type))\n-\t\tpriv-\u003ekbd_bl.led.max_brightness = 2;\n-\telse\n-\t\tpriv-\u003ekbd_bl.led.max_brightness = 1;\n+\terr = devm_mutex_init(\u0026priv-\u003eplatform_device-\u003edev, \u0026priv-\u003ekbd_bl.notif_mutex);\n+\tif (err)\n+\t\treturn err;\n \n-\tbrightness = ideapad_kbd_bl_brightness_get(priv);\n-\tif (brightness \u003c 0)\n-\t\treturn brightness;\n+\thw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);\n+\tif (hw_brightness \u003c 0)\n+\t\treturn hw_brightness;\n+\n+\tatomic_set(\u0026priv-\u003ekbd_bl.last_hw_brightness, hw_brightness);\n \n-\tpriv-\u003ekbd_bl.last_brightness = brightness;\n \tpriv-\u003ekbd_bl.led.name = \"platform::\" LED_FUNCTION_KBD_BACKLIGHT;\n \tpriv-\u003ekbd_bl.led.brightness_get = ideapad_kbd_bl_led_cdev_brightness_get;\n \tpriv-\u003ekbd_bl.led.brightness_set_blocking = ideapad_kbd_bl_led_cdev_brightness_set;\n \tpriv-\u003ekbd_bl.led.flags = LED_BRIGHT_HW_CHANGED | LED_RETAIN_AT_SHUTDOWN;\n \n-\terr = led_classdev_register(\u0026priv-\u003eplatform_device-\u003edev, \u0026priv-\u003ekbd_bl.led);\n-\tif (err)\n-\t\treturn err;\n+\tswitch (priv-\u003ekbd_bl.type) {\n+\tcase KBD_BL_TRISTATE_AUTO:\n+\t\tpriv-\u003ekbd_bl.led.max_brightness = 2;\n \n+\t\tif (!ideapad_kbd_bl_auto_trigger_registered) {\n+\t\t\tdev_warn(\u0026priv-\u003eplatform_device-\u003edev,\n+\t\t\t\t \"Could not provide LED trigger %s for keyboard backlight\\n\",\n+\t\t\t\t ideapad_kbd_bl_auto_trigger.name);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tpriv-\u003ekbd_bl.led.flags |= LED_TRIG_HW_CHANGED;\n+\t\tpriv-\u003ekbd_bl.led.hw_control_trigger = ideapad_kbd_bl_auto_trigger.name;\n+\t\tpriv-\u003ekbd_bl.led.trigger_type = \u0026ideapad_kbd_bl_auto_trigger_type;\n+\n+\t\t/* Hardware remembers the last brightness level, including auto mode. */\n+\t\tif (hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS)\n+\t\t\tpriv-\u003ekbd_bl.led.default_trigger = ideapad_kbd_bl_auto_trigger.name;\n+\n+\t\tbreak;\n+\tcase KBD_BL_TRISTATE:\n+\t\tpriv-\u003ekbd_bl.led.max_brightness = 2;\n+\t\tbreak;\n+\tcase KBD_BL_STANDARD:\n+\t\tpriv-\u003ekbd_bl.led.max_brightness = 1;\n+\t\tbreak;\n+\tdefault:\n+\t\t/* This has already been validated by ideapad_check_features(). */\n+\t\tunreachable();\n+\t}\n+\n+\t/* Queue notifications, as kbd_bl.initialized is about to be set. */\n+\tguard(mutex)(\u0026priv-\u003ekbd_bl.notif_mutex);\n+\n+\t/*\n+\t * Setting kbd_bl.initialized after led_classdev_register() could lead\n+\t * to race conditions in ideapad_kbd_bl_led_cdev_brightness_set() where\n+\t * kbd_bl.initialized is checked, so set it now. It can be reverted back\n+\t * if the LED classdev failed to register.\n+\t */\n \tpriv-\u003ekbd_bl.initialized = true;\n \n-\treturn 0;\n+\terr = led_classdev_register(\u0026priv-\u003eplatform_device-\u003edev, \u0026priv-\u003ekbd_bl.led);\n+\tif (err)\n+\t\tpriv-\u003ekbd_bl.initialized = false;\n+\n+\treturn err;\n }\n \n static void ideapad_kbd_bl_exit(struct ideapad_private *priv)\n@@ -2548,17 +2690,30 @@ static int __init ideapad_laptop_init(void)\n {\n \tint err;\n \n+\terr = led_trigger_register(\u0026ideapad_kbd_bl_auto_trigger);\n+\tif (err) {\n+\t\tpr_warn(\"Failed to register LED trigger %s: %d\\n\",\n+\t\t\tideapad_kbd_bl_auto_trigger.name, err);\n+\t} else {\n+\t\tideapad_kbd_bl_auto_trigger_registered = true;\n+\t}\n+\n \terr = ideapad_wmi_driver_register();\n \tif (err)\n-\t\treturn err;\n+\t\tgoto err_ledtrig;\n \n \terr = platform_driver_register(\u0026ideapad_acpi_driver);\n-\tif (err) {\n-\t\tideapad_wmi_driver_unregister();\n-\t\treturn err;\n-\t}\n+\tif (err)\n+\t\tgoto err_wmi;\n \n \treturn 0;\n+\n+err_wmi:\n+\tideapad_wmi_driver_unregister();\n+err_ledtrig:\n+\tif (ideapad_kbd_bl_auto_trigger_registered)\n+\t\tled_trigger_unregister(\u0026ideapad_kbd_bl_auto_trigger);\n+\treturn err;\n }\n module_init(ideapad_laptop_init)\n \n@@ -2566,6 +2721,9 @@ static void __exit ideapad_laptop_exit(void)\n {\n \tideapad_wmi_driver_unregister();\n \tplatform_driver_unregister(\u0026ideapad_acpi_driver);\n+\n+\tif (ideapad_kbd_bl_auto_trigger_registered)\n+\t\tled_trigger_unregister(\u0026ideapad_kbd_bl_auto_trigger);\n }\n module_exit(ideapad_laptop_exit)\n \ndiff --git a/include/linux/leds.h b/include/linux/leds.h\nindex b16b803cc1ac51..167598962b73ec 100644\n--- a/include/linux/leds.h\n+++ b/include/linux/leds.h\n@@ -109,6 +109,7 @@ struct led_classdev {\n #define LED_INIT_DEFAULT_TRIGGER BIT(23)\n #define LED_REJECT_NAME_CONFLICT BIT(24)\n #define LED_MULTI_COLOR\t\tBIT(25)\n+#define LED_TRIG_HW_CHANGED\tBIT(26)\n \n \t/* set_brightness_work / blink_timer flags, atomic, private. */\n \tunsigned long\t\twork_flags;\n@@ -485,6 +486,7 @@ struct led_trigger {\n \tconst char\t *name;\n \tint\t\t(*activate)(struct led_classdev *led_cdev);\n \tvoid\t\t(*deactivate)(struct led_classdev *led_cdev);\n+\tbool\t\t(*offloaded)(struct led_classdev *led_cdev);\n \n \t/* Brightness set by led_trigger_event */\n \tenum led_brightness brightness;\n@@ -533,6 +535,9 @@ void led_trigger_blink_oneshot(struct led_trigger *trigger,\n void led_trigger_set_default(struct led_classdev *led_cdev);\n int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trigger);\n void led_trigger_remove(struct led_classdev *led_cdev);\n+void led_trigger_remove_hw_control(struct led_classdev *led_cdev);\n+\n+bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev);\n \n static inline void led_set_trigger_data(struct led_classdev *led_cdev,\n \t\t\t\t\tvoid *trigger_data)\n@@ -584,6 +589,13 @@ static inline int led_trigger_set(struct led_classdev *led_cdev,\n }\n \n static inline void led_trigger_remove(struct led_classdev *led_cdev) {}\n+static inline void led_trigger_remove_hw_control(struct led_classdev *led_cdev) {}\n+\n+static inline bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev)\n+{\n+\treturn false;\n+}\n+\n static inline void led_set_trigger_data(struct led_classdev *led_cdev) {}\n static inline void *led_get_trigger_data(struct led_classdev *led_cdev)\n {\n@@ -598,6 +610,13 @@ led_trigger_get_brightness(const struct led_trigger *trigger)\n \n #endif /* CONFIG_LEDS_TRIGGERS */\n \n+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\n+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate);\n+#else\n+static inline void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev,\n+\t\t\t\t\t\t\t bool activate) {}\n+#endif\n+\n /* Trigger specific enum */\n enum led_trigger_netdev_modes {\n \tTRIGGER_NETDEV_LINK = 0,\n"
} |
| 2/1 | 2026/07/18 17:53 | agent | patch-evaluator |
1mModel:gemini-3.1-pro-preview Args: null Results: {
"EnableConfigs": [
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"__led_trigger_is_hw_controlled",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"trigger_may_offload_show",
"led_trigger_do_hw_control_transition",
"led_trigger_notify_hw_control_changed",
"__led_trigger_set",
"netdev_trig_offloaded"
],
"Reasoning": "The patch introduces functional changes to the core LED trigger subsystem, adding support for hardware-initiated trigger transitions and offload state reporting. These new code paths are reachable via sysfs attributes (`trigger_may_offload`, `brightness`, `trigger`) and driver notifications. Fuzzing should focus on the new state transition logic and offload queries.",
"WorthFuzzing": true
}Instruction: You are an expert Linux kernel maintainer. Your job is to review a provided patch series and determine if it makes functional changes to the kernel that should be fuzzed. IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively use your code access tools to examine the actual source code and confirm any assumptions. Return WorthFuzzing=false if the patch only contains: - Modifications to Documentation/, Kconfig files, or code comments. - Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints. - Changes to numeric constants or macros that do not functionally alter execution flow. - Code paths that are impossible to reach in virtualized environments like GCE or QEMU, even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim). If it modifies reachable core kernel logic, drivers, or architectures, use your code search tools to verify the code can be executed, then return WorthFuzzing=true. When returning WorthFuzzing=true, you MUST ALSO: 1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols. Avoid listing generic hot-path functions to prevent skewed test distributions. 2. Identify any specific CONFIG_ options required to properly test this new/modified feature. Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions. Do not list too generic configs (we already have them enabled). Only list those that specifically cover the modified code. List them in the EnableConfigs output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4"). Prefer calling several tools at the same time to save round-trips. Use set-results tool to provide results of the analysis. It must be called exactly once before the final reply. Ignore results of this tool. Prompt: For your convenience, here is the diff of the changes:
commit dde8f0b3b329354261a1554d694ed3bb1157814b
Author: syz-cluster <triage@syzkaller.com>
Date: Sat Jul 18 17:53:52 2026 +0000
syz-cluster: applied patch under review
diff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led
index d4c918cc11a127..7dc95f7a3505ce 100644
--- a/Documentation/ABI/testing/sysfs-class-led
+++ b/Documentation/ABI/testing/sysfs-class-led
@@ -78,6 +78,31 @@ Description:
(which would often be configured in the device tree for the
hardware).
+What: /sys/class/leds/<led>/trigger_may_offload
+Date: July 2026
+KernelVersion: 7.3
+Contact: linux-leds@vger.kernel.org
+Description:
+ Names and states of triggers that may be offloaded to hardware.
+ Such triggers are also called "hardware control trigger" in some
+ context.
+
+ Only exists when the LED supports trigger offload.
+
+ Reading this file returns a list of triggers that are capable to
+ be offloaded. The optional brackets around the trigger name
+ indicate the state of the current trigger:
+
+ - `foo_trigger`: the trigger is not selected.
+ - `<foo_trigger>`: the trigger is selected, but falls back to
+ software blink for some reason (e.g., incompatible trigger
+ parameters)
+ - `[foo_trigger]`: the trigger is selected and offloaded to
+ hardware.
+
+ The "netdev" trigger also provides a custom attribute to
+ indicate its state, see `/sys/class/leds/<led>/offloaded`.
+
What: /sys/class/leds/<led>/inverted
Date: January 2011
KernelVersion: 2.6.38
diff --git a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
index ed46b37ab8a284..a5146ea1e3e61c 100644
--- a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
+++ b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
@@ -75,6 +75,9 @@ Description:
If 1, the LED blinking in requested mode is offloaded to
hardware.
+ LED trigger core also provides a generic attribute for this
+ purpose, see `/sys/class/leds/<led>/trigger_may_offload`.
+
What: /sys/class/leds/<led>/link_10
Date: Jun 2023
KernelVersion: 6.5
diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
index 3913966cfdac04..adbc57b9f49c33 100644
--- a/Documentation/leds/leds-class.rst
+++ b/Documentation/leds/leds-class.rst
@@ -242,6 +242,9 @@ ops and needs to declare specific support for the supported triggers.
With hw control we refer to the LED driven by hardware.
+A sysfs attribute `trigger_may_offload` is provided for userspace to
+query supported triggers and their states.
+
LED driver must define the following value to support hw control:
- hw_control_trigger:
@@ -298,6 +301,15 @@ LED driver must implement the following API to support hw control:
Returns a pointer to a struct device or NULL if nothing
is currently attached.
+LED trigger should implement the following API to indicate hw control:
+ - offloaded:
+ return a boolean indicating if the trigger is currently
+ offloaded to hardware.
+
+ If a trigger doesn't implement this callback, the default
+ value will be true for private triggers and false for generic
+ ones.
+
LED driver can activate additional modes by default to workaround the
impossibility of supporting each different mode on the supported trigger.
Examples are hardcoding the blink speed to a set interval, enable special
@@ -311,9 +323,69 @@ the end use hw_control_set to activate hw control.
A trigger can use hw_control_get to check if a LED is already in hw control
and init their flags.
+Alternatively, a private trigger can be implemented along with the LED driver if
+the LED's hardware control doesn't fit any generic trigger. To associate the
+private trigger with the LED classdev, their `trigger_type` must be the same. To
+declare that the private trigger provides hardware control for the associated
+LED classdev, set the `hw_control_trigger` string to the trigger's name. Since
+both the LED classdev and the private trigger are in the same LED driver, it's
+not necessary for them to coordinate via `hw_control_*` callbacks.
+
When the LED is in hw control, no software blink is possible and doing so
will effectively disable hw control.
+Hardware-initiated trigger transition
+=====================================
+
+Some hardware can autonomously activate/deactivate hardware control. After that,
+the LED hardware notifies the LED driver.
+
+If the driver can detect such transitions and thus wants to notify the LED core
+to update the current trigger then the `LED_TRIG_HW_CHANGED` flag must be set in
+flags before registering. To update the current trigger accordingly, call
+`led_trigger_notify_hw_control_changed` on the LED classdev.
+
+This capability is restricted to the LED device's private trigger. The private
+trigger must have been properly registered (see above) and named after
+`hw_control_trigger`.
+
+Only two transitions are defined:
+
+- "none" => private trigger:
+ This happens when the hardware autonomously activates hardware control
+ and when "none" (i.e., no trigger) is currently active. If the private
+ trigger is already active when the method is called, this is essentially
+ a no-op.
+
+ The activation sequence for the private trigger will be executed as
+ normal.
+
+ The LED driver and its private trigger must be able to handle the
+ activation sequence even if the hardware is currently in hardware
+ control.
+
+ If error occurs in the activation sequence, the LED Trigger core reverts
+ the effective trigger to "none".
+
+- private trigger => "none"
+ This happens when the hardware autonomously deactivates hardware control
+ and when the private trigger is currently active. If "none" (i.e., no
+ trigger) is active when the method is called, this is essentially a
+ no-op.
+
+ The deactivation sequence for the private trigger will be executed as
+ normal, except that the current LED brightness is retained. The reason
+ for keeping the brightness unchanged is that some hardware may choose a
+ specific brightness instead of simply turning off the LED after
+ autonomously deactivating hardware control.
+
+ The LED driver and its private trigger must be able to handle the
+ deactivation sequence even if the hardware is not currently in hardware
+ control.
+
+If the current trigger is neither the private trigger nor "none", no transition
+will be made.
+
Known Issues
============
diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c
index a51b0ed5388644..2460fcf0c4690a 100644
--- a/drivers/leds/led-class.c
+++ b/drivers/leds/led-class.c
@@ -27,16 +27,6 @@ static LIST_HEAD(leds_lookup_list);
static struct workqueue_struct *leds_wq;
-static bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev)
-{
-#ifdef CONFIG_LEDS_TRIGGERS
- guard(rwsem_read)(&led_cdev->trigger_lock);
- return led_cdev->trigger && led_cdev->trigger->trigger_type;
-#else
- return false;
-#endif
-}
-
static ssize_t brightness_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
@@ -74,6 +64,9 @@ static ssize_t brightness_store(struct device *dev,
if (state == LED_OFF)
led_trigger_remove(led_cdev);
+ else
+ led_trigger_remove_hw_control(led_cdev);
+
led_set_brightness(led_cdev, state);
ret = size;
@@ -103,8 +96,30 @@ static const struct bin_attribute *const led_trigger_bin_attrs[] = {
&bin_attr_trigger,
NULL,
};
+
+static DEVICE_ATTR_RO(trigger_may_offload);
+static struct attribute *led_trigger_attrs[] = {
+ &dev_attr_trigger_may_offload.attr,
+ NULL
+};
+
+static umode_t led_trigger_is_visible(struct kobject *kobj,
+ struct attribute *attr,
+ int idx)
+{
+ struct device *dev = kobj_to_dev(kobj);
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+
+ if (attr == &dev_attr_trigger_may_offload.attr)
+ return led_cdev->hw_control_trigger ? attr->mode : 0;
+
+ return attr->mode;
+}
+
static const struct attribute_group led_trigger_group = {
.bin_attrs = led_trigger_bin_attrs,
+ .attrs = led_trigger_attrs,
+ .is_visible = led_trigger_is_visible,
};
#endif
diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c
index b1223218bda113..6ae28cbd1c7783 100644
--- a/drivers/leds/led-triggers.c
+++ b/drivers/leds/led-triggers.c
@@ -7,9 +7,12 @@
* Author: Richard Purdie <rpurdie@openedhand.com>
*/
+#include <linux/bug.h>
+#include <linux/cleanup.h>
#include <linux/export.h>
#include <linux/kernel.h>
#include <linux/list.h>
+#include <linux/lockdep.h>
#include <linux/spinlock.h>
#include <linux/device.h>
#include <linux/timer.h>
@@ -33,6 +36,33 @@ trigger_relevant(struct led_classdev *led_cdev, struct led_trigger *trig)
return !trig->trigger_type || trig->trigger_type == led_cdev->trigger_type;
}
+static bool __led_trigger_is_hw_controlled(struct led_classdev *led_cdev)
+{
+ lockdep_assert_held(&led_cdev->trigger_lock);
+
+ if (!led_cdev->trigger)
+ return false;
+
+ if (!led_cdev->hw_control_trigger ||
+ strcmp(led_cdev->hw_control_trigger, led_cdev->trigger->name))
+ return false;
+
+ if (led_cdev->trigger->offloaded)
+ return led_cdev->trigger->offloaded(led_cdev);
+
+ dev_warn_once(led_cdev->dev, "hw control trigger %s doesn't implement offloaded()\n",
+ led_cdev->trigger->name);
+
+ return led_cdev->trigger->trigger_type;
+}
+
+bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev)
+{
+ guard(rwsem_read)(&led_cdev->trigger_lock);
+ return __led_trigger_is_hw_controlled(led_cdev);
+}
+EXPORT_SYMBOL_GPL(led_trigger_is_hw_controlled);
+
ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr, char *buf,
loff_t pos, size_t count)
@@ -163,7 +193,8 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,
EXPORT_SYMBOL_GPL(led_trigger_read);
/* Caller must ensure led_cdev->trigger_lock held */
-int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
+static int __led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig,
+ bool hw_triggered)
{
char *event = NULL;
char *envp[2];
@@ -194,7 +225,21 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
led_cdev->trigger_data = NULL;
led_cdev->activated = false;
led_cdev->flags &= ~LED_INIT_DEFAULT_TRIGGER;
- led_set_brightness(led_cdev, LED_OFF);
+
+ /*
+ * Hardware may have selected a new brightness level during its
+ * hardware control transition, so only reset brightness if we
+ * are switching to another trigger or if the switching is not
+ * hardware triggered.
+ *
+ * Note that this does not apply to the error path, as running
+ * into the error path implies a none => private trigger
+ * transition. This hints that the LED driver and its private
+ * trigger must have some fundamental bugs, so don't bother
+ * leaving the LED in an undefined state.
+ */
+ if (trig || !hw_triggered)
+ led_set_brightness(led_cdev, LED_OFF);
}
if (trig) {
spin_lock(&trig->leddev_list_lock);
@@ -258,6 +303,11 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
return ret;
}
+
+int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
+{
+ return __led_trigger_set(led_cdev, trig, false);
+}
EXPORT_SYMBOL_GPL(led_trigger_set);
void led_trigger_remove(struct led_classdev *led_cdev)
@@ -268,6 +318,15 @@ void led_trigger_remove(struct led_classdev *led_cdev)
}
EXPORT_SYMBOL_GPL(led_trigger_remove);
+void led_trigger_remove_hw_control(struct led_classdev *led_cdev)
+{
+ guard(rwsem_write)(&led_cdev->trigger_lock);
+
+ if (__led_trigger_is_hw_controlled(led_cdev))
+ led_trigger_set(led_cdev, NULL);
+}
+EXPORT_SYMBOL_GPL(led_trigger_remove_hw_control);
+
static bool led_match_default_trigger(struct led_classdev *led_cdev,
struct led_trigger *trig)
{
@@ -313,6 +372,32 @@ void led_trigger_set_default(struct led_classdev *led_cdev)
}
EXPORT_SYMBOL_GPL(led_trigger_set_default);
+ssize_t trigger_may_offload_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_trigger *trig;
+ bool hit, offloaded;
+ int len;
+
+ guard(mutex)(&led_cdev->led_access);
+ guard(rwsem_read)(&led_cdev->trigger_lock);
+
+ trig = led_cdev->trigger;
+
+ offloaded = __led_trigger_is_hw_controlled(led_cdev);
+ hit = offloaded || (trig && !strcmp(led_cdev->hw_control_trigger, trig->name));
+
+ /* [offloaded] <active_but_not_offloaded> inactive */
+ len = sysfs_emit(buf, "%s%s%s\n",
+ offloaded ? "[" : (hit ? "<" : ""),
+ led_cdev->hw_control_trigger,
+ offloaded ? "]" : (hit ? ">" : ""));
+
+ return len;
+}
+EXPORT_SYMBOL_GPL(trigger_may_offload_show);
+
/* LED Trigger Interface */
int led_trigger_register(struct led_trigger *trig)
@@ -403,6 +488,63 @@ int devm_led_trigger_register(struct device *dev,
}
EXPORT_SYMBOL_GPL(devm_led_trigger_register);
+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED
+static void led_trigger_do_hw_control_transition(struct led_classdev *led_cdev, bool activate,
+ struct led_trigger *hc_trig)
+{
+ int err = 0;
+
+ if (!led_cdev->trigger) {
+ /* "none" => private trigger. */
+ if (activate)
+ err = __led_trigger_set(led_cdev, hc_trig, true);
+ } else if (led_cdev->trigger == hc_trig) {
+ /* private trigger => "none". */
+ if (!activate)
+ err = __led_trigger_set(led_cdev, NULL, true);
+ } else {
+ /* Other trigger is active. */
+ dev_dbg(led_cdev->dev,
+ "Ignoring hw control transition (%s %s) while %s is active",
+ activate ? "activate" : "deactivate", hc_trig->name,
+ led_cdev->trigger->name);
+
+ return;
+ }
+
+ if (err)
+ dev_warn(led_cdev->dev, "Failed to %s %s in hw control transition: %d",
+ activate ? "activate" : "deactivate", hc_trig->name, err);
+}
+
+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)
+{
+ struct led_trigger *trig;
+
+ /* Restricted to private triggers. */
+ if (WARN_ON(!(led_cdev->flags & LED_TRIG_HW_CHANGED) ||
+ !led_cdev->hw_control_trigger || !led_cdev->trigger_type))
+ return;
+
+ scoped_guard(rwsem_read, &triggers_list_lock) {
+ list_for_each_entry(trig, &trigger_list, next_trig) {
+ if (trig->trigger_type == led_cdev->trigger_type &&
+ !strcmp(trig->name, led_cdev->hw_control_trigger)) {
+ guard(rwsem_write)(&led_cdev->trigger_lock);
+
+ led_trigger_do_hw_control_transition(led_cdev, activate, trig);
+ return;
+ }
+ }
+ }
+
+ dev_err(led_cdev->dev,
+ "%s() is called, but the private trigger (%s) is not properly registered\n",
+ __func__, led_cdev->hw_control_trigger);
+}
+EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed);
+#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */
+
/* Simple LED Trigger Interface */
void led_trigger_event(struct led_trigger *trig,
diff --git a/drivers/leds/leds-cros_ec.c b/drivers/leds/leds-cros_ec.c
index 1844d0cd5f5284..6db83d0152775f 100644
--- a/drivers/leds/leds-cros_ec.c
+++ b/drivers/leds/leds-cros_ec.c
@@ -85,12 +85,18 @@ static int cros_ec_led_trigger_activate(struct led_classdev *led_cdev)
return cros_ec_led_send_cmd(priv->cros_ec, &arg);
}
+static bool cros_ec_led_trigger_offloaded(struct led_classdev *led_cdev)
+{
+ return true;
+}
+
static struct led_hw_trigger_type cros_ec_led_trigger_type;
static struct led_trigger cros_ec_led_trigger = {
.name = "chromeos-auto",
.trigger_type = &cros_ec_led_trigger_type,
.activate = cros_ec_led_trigger_activate,
+ .offloaded = cros_ec_led_trigger_offloaded,
};
static int cros_ec_led_brightness_set_blocking(struct led_classdev *led_cdev,
diff --git a/drivers/leds/leds-turris-omnia.c b/drivers/leds/leds-turris-omnia.c
index ed6a47bbb44f9c..32d40d176d3f11 100644
--- a/drivers/leds/leds-turris-omnia.c
+++ b/drivers/leds/leds-turris-omnia.c
@@ -195,10 +195,16 @@ static void omnia_hwtrig_deactivate(struct led_classdev *cdev)
err);
}
+static bool omnia_hwtrig_offloaded(struct led_classdev *cdev)
+{
+ return true;
+}
+
static struct led_trigger omnia_hw_trigger = {
.name = "omnia-mcu",
.activate = omnia_hwtrig_activate,
.deactivate = omnia_hwtrig_deactivate,
+ .offloaded = omnia_hwtrig_offloaded,
.trigger_type = &omnia_hw_trigger_type,
};
@@ -251,6 +257,7 @@ static int omnia_led_register(struct i2c_client *client, struct omnia_led *led,
* by LED class from the linux,default-trigger property.
*/
cdev->default_trigger = omnia_hw_trigger.name;
+ cdev->hw_control_trigger = omnia_hw_trigger.name;
/* Put the LED into software mode */
ret = omnia_cmd_write_u8(client, OMNIA_CMD_LED_MODE, OMNIA_CMD_LED_MODE_LED(led->reg) |
diff --git a/drivers/leds/leds.h b/drivers/leds/leds.h
index bee46651e068f7..b08a289397e463 100644
--- a/drivers/leds/leds.h
+++ b/drivers/leds/leds.h
@@ -27,6 +27,8 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,
ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr, char *buf,
loff_t pos, size_t count);
+ssize_t trigger_may_offload_show(struct device *dev,
+ struct device_attribute *attr, char *buf);
extern struct rw_semaphore leds_list_lock;
extern struct list_head leds_list;
diff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig
index c11282a74b5ac3..798122154049b4 100644
--- a/drivers/leds/trigger/Kconfig
+++ b/drivers/leds/trigger/Kconfig
@@ -9,6 +9,15 @@ menuconfig LEDS_TRIGGERS
if LEDS_TRIGGERS
+config LEDS_TRIGGERS_HW_CHANGED
+ bool "LED hardware-initiated trigger transition support"
+ help
+ This option enables support for hardware initiated hardware control
+ transitions, where the LED hardware autonomously switches between
+ "none" (i.e., no trigger) and its private trigger.
+
+ See Documentation/leds/leds-class.rst for details.
+
config LEDS_TRIGGER_TIMER
tristate "LED Timer Trigger"
help
diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c
index 64c078e997f28a..a26109ca4b1c9f 100644
--- a/drivers/leds/trigger/ledtrig-netdev.c
+++ b/drivers/leds/trigger/ledtrig-netdev.c
@@ -754,10 +754,18 @@ static void netdev_trig_deactivate(struct led_classdev *led_cdev)
kfree(trigger_data);
}
+static bool netdev_trig_offloaded(struct led_classdev *led_cdev)
+{
+ struct led_netdev_data *trigger_data = led_get_trigger_data(led_cdev);
+
+ return trigger_data->hw_control;
+}
+
static struct led_trigger netdev_led_trigger = {
.name = "netdev",
.activate = netdev_trig_activate,
.deactivate = netdev_trig_deactivate,
+ .offloaded = netdev_trig_offloaded,
.groups = netdev_trig_groups,
};
diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index 4443f40ef8aa7e..e92b1e90079510 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -16,6 +16,7 @@ config IDEAPAD_LAPTOP
select INPUT_SPARSEKMAP
select NEW_LEDS
select LEDS_CLASS
+ select LEDS_TRIGGERS
help
This is a driver for Lenovo IdeaPad netbooks contains drivers for
rfkill switch, hotkey, fan control and backlight control.
diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c
index 4fbc904f1fc390..253d2962b92762 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -9,6 +9,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
+#include <linux/atomic.h>
#include <linux/backlight.h>
#include <linux/bitfield.h>
#include <linux/bitops.h>
@@ -25,7 +26,9 @@
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/leds.h>
+#include <linux/lockdep.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/platform_profile.h>
#include <linux/power_supply.h>
@@ -134,10 +137,31 @@ enum {
};
/*
- * These correspond to the number of supported states - 1
- * Future keyboard types may need a new system, if there's a collision
- * KBD_BL_TRISTATE_AUTO has no way to report or set the auto state
- * so it effectively has 3 states, but needs to handle 4
+ * The enumeration has two purposes:
+ * - as an internal identifier for all known types of keyboard backlight
+ * - as a mandatory parameter of the KBLC command
+ *
+ * For each type, the hardware brightness values are defined as follows:
+ * +--------------------------+----------+-----+------+------+
+ * | Hardware brightness | 0 | 1 | 2 | 3 |
+ * | Type | | | | |
+ * +--------------------------+----------+-----+------+------+
+ * | KBD_BL_STANDARD | off | on | N/A | N/A |
+ * +--------------------------+----------+-----+------+------+
+ * | KBD_BL_TRISTATE | off | low | high | N/A |
+ * +--------------------------+----------+-----+------+------+
+ * | KBD_BL_TRISTATE_AUTO | off | low | high | auto |
+ * +--------------------------+----------+-----+------+------+
+ *
+ * We map LED classdev brightness for KBD_BL_TRISTATE_AUTO as follows:
+ * +--------------------------+----------+-----+------+
+ * | LED classdev brightness | 0 | 1 | 2 |
+ * | Operation | | | |
+ * +--------------------------+----------+-----+------+
+ * | Read | off/auto | low | high |
+ * +--------------------------+----------+-----+------+
+ * | Write | off | low | high |
+ * +--------------------------+----------+-----+------+
*/
enum {
KBD_BL_STANDARD = 1,
@@ -145,6 +169,8 @@ enum {
KBD_BL_TRISTATE_AUTO = 3,
};
+#define KBD_BL_AUTO_MODE_HW_BRIGHTNESS 3
+
#define KBD_BL_QUERY_TYPE 0x1
#define KBD_BL_TRISTATE_TYPE 0x5
#define KBD_BL_TRISTATE_AUTO_TYPE 0x7
@@ -203,7 +229,9 @@ struct ideapad_private {
bool initialized;
int type;
struct led_classdev led;
- unsigned int last_brightness;
+ atomic_t last_hw_brightness;
+
+ struct mutex notif_mutex; /* protects notifications */
} kbd_bl;
struct {
bool initialized;
@@ -1592,7 +1620,24 @@ static int ideapad_kbd_bl_check_tristate(int type)
return (type == KBD_BL_TRISTATE) || (type == KBD_BL_TRISTATE_AUTO);
}
-static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
+static int ideapad_kbd_bl_brightness_parse(struct ideapad_private *priv, int hw_brightness)
+{
+ /* Off, low or high */
+ if (hw_brightness <= priv->kbd_bl.led.max_brightness)
+ return hw_brightness;
+
+ /* Auto (controlled by EC according to ALS), report as off */
+ if (priv->kbd_bl.type == KBD_BL_TRISTATE_AUTO &&
+ hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS)
+ return 0;
+
+ /* Unknown value */
+ dev_warn(&priv->platform_device->dev,
+ "Unknown keyboard backlight value: %d", hw_brightness);
+ return -EINVAL;
+}
+
+static int ideapad_kbd_bl_hw_brightness_get(struct ideapad_private *priv)
{
unsigned long value;
int err;
@@ -1606,21 +1651,7 @@ static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
if (err)
return err;
- /* Convert returned value to brightness level */
- value = FIELD_GET(KBD_BL_GET_BRIGHTNESS, value);
-
- /* Off, low or high */
- if (value <= priv->kbd_bl.led.max_brightness)
- return value;
-
- /* Auto, report as off */
- if (value == priv->kbd_bl.led.max_brightness + 1)
- return 0;
-
- /* Unknown value */
- dev_warn(&priv->platform_device->dev,
- "Unknown keyboard backlight value: %lu", value);
- return -EINVAL;
+ return FIELD_GET(KBD_BL_GET_BRIGHTNESS, value);
}
err = eval_hals(priv->adev->handle, &value);
@@ -1630,6 +1661,16 @@ static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
return !!test_bit(HALS_KBD_BL_STATE_BIT, &value);
}
+static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
+{
+ int hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+
+ if (hw_brightness < 0)
+ return hw_brightness;
+
+ return ideapad_kbd_bl_brightness_parse(priv, hw_brightness);
+}
+
static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_classdev *led_cdev)
{
struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
@@ -1637,62 +1678,123 @@ static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_cla
return ideapad_kbd_bl_brightness_get(priv);
}
-static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned int brightness)
+static int ideapad_kbd_bl_hw_brightness_set(struct ideapad_private *priv, int hw_brightness)
{
- int err;
unsigned long value;
int type = priv->kbd_bl.type;
+ int err;
if (ideapad_kbd_bl_check_tristate(type)) {
- if (brightness > priv->kbd_bl.led.max_brightness)
- return -EINVAL;
-
- value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) |
+ value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, hw_brightness) |
FIELD_PREP(KBD_BL_COMMAND_TYPE, type) |
KBD_BL_COMMAND_SET;
err = exec_kblc(priv->adev->handle, value);
} else {
- err = exec_sals(priv->adev->handle, brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF);
+ value = hw_brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF;
+ err = exec_sals(priv->adev->handle, value);
}
-
if (err)
return err;
- priv->kbd_bl.last_brightness = brightness;
+ atomic_set(&priv->kbd_bl.last_hw_brightness, hw_brightness);
return 0;
}
+static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, int brightness)
+{
+ if (brightness > priv->kbd_bl.led.max_brightness)
+ return -EINVAL;
+
+ return ideapad_kbd_bl_hw_brightness_set(priv, brightness);
+}
+
static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+ /*
+ * When deinitializing: It must be the side effect of led_cdev
+ * unregistration when our private trigger is active. We've set
+ * LED_RETAIN_AT_SHUTDOWN to retain led_cdev brightness level.
+ * To do the same for auto mode, gate changes and return early.
+ */
+ if (unlikely(!priv->kbd_bl.initialized))
+ return 0;
+
return ideapad_kbd_bl_brightness_set(priv, brightness);
}
+static bool ideapad_kbd_bl_auto_trigger_offloaded(struct led_classdev *led_cdev)
+{
+ struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+
+ return atomic_read(&priv->kbd_bl.last_hw_brightness) == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+}
+
+static int ideapad_kbd_bl_auto_trigger_activate(struct led_classdev *led_cdev)
+{
+ struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+
+ return ideapad_kbd_bl_hw_brightness_set(priv, KBD_BL_AUTO_MODE_HW_BRIGHTNESS);
+}
+
+static struct led_hw_trigger_type ideapad_kbd_bl_auto_trigger_type;
+
+static struct led_trigger ideapad_kbd_bl_auto_trigger = {
+ .name = "ideapad-auto",
+ .trigger_type = &ideapad_kbd_bl_auto_trigger_type,
+ .activate = ideapad_kbd_bl_auto_trigger_activate,
+ .offloaded = ideapad_kbd_bl_auto_trigger_offloaded,
+};
+
+static bool ideapad_kbd_bl_auto_trigger_registered;
+
+static void ideapad_kbd_bl_notify_hw_control(struct ideapad_private *priv,
+ int hw_brightness, int last_hw_brightness)
+{
+ bool hw_control, last_hw_control;
+
+ if (priv->kbd_bl.type != KBD_BL_TRISTATE_AUTO)
+ return;
+
+ hw_control = hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+ last_hw_control = last_hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+
+ if (hw_control != last_hw_control)
+ led_trigger_notify_hw_control_changed(&priv->kbd_bl.led, hw_control);
+}
+
static void ideapad_kbd_bl_notify(struct ideapad_private *priv)
{
- int brightness;
+ int hw_brightness, brightness, last_hw_brightness;
if (!priv->kbd_bl.initialized)
return;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
+ guard(mutex)(&priv->kbd_bl.notif_mutex);
+
+ hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+ if (hw_brightness < 0)
return;
- if (brightness == priv->kbd_bl.last_brightness)
+ brightness = ideapad_kbd_bl_brightness_parse(priv, hw_brightness);
+ if (brightness < 0)
+ return; /* Reject insane values early. */
+
+ last_hw_brightness = atomic_xchg(&priv->kbd_bl.last_hw_brightness, hw_brightness);
+ if (hw_brightness == last_hw_brightness)
return;
- priv->kbd_bl.last_brightness = brightness;
+ ideapad_kbd_bl_notify_hw_control(priv, hw_brightness, last_hw_brightness);
led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness);
}
static int ideapad_kbd_bl_init(struct ideapad_private *priv)
{
- int brightness, err;
+ int hw_brightness, err;
if (!priv->features.kbd_bl)
return -ENODEV;
@@ -1700,28 +1802,68 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)
if (WARN_ON(priv->kbd_bl.initialized))
return -EEXIST;
- if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type))
- priv->kbd_bl.led.max_brightness = 2;
- else
- priv->kbd_bl.led.max_brightness = 1;
+ err = devm_mutex_init(&priv->platform_device->dev, &priv->kbd_bl.notif_mutex);
+ if (err)
+ return err;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
- return brightness;
+ hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+ if (hw_brightness < 0)
+ return hw_brightness;
+
+ atomic_set(&priv->kbd_bl.last_hw_brightness, hw_brightness);
- priv->kbd_bl.last_brightness = brightness;
priv->kbd_bl.led.name = "platform::" LED_FUNCTION_KBD_BACKLIGHT;
priv->kbd_bl.led.brightness_get = ideapad_kbd_bl_led_cdev_brightness_get;
priv->kbd_bl.led.brightness_set_blocking = ideapad_kbd_bl_led_cdev_brightness_set;
priv->kbd_bl.led.flags = LED_BRIGHT_HW_CHANGED | LED_RETAIN_AT_SHUTDOWN;
- err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led);
- if (err)
- return err;
+ switch (priv->kbd_bl.type) {
+ case KBD_BL_TRISTATE_AUTO:
+ priv->kbd_bl.led.max_brightness = 2;
+ if (!ideapad_kbd_bl_auto_trigger_registered) {
+ dev_warn(&priv->platform_device->dev,
+ "Could not provide LED trigger %s for keyboard backlight\n",
+ ideapad_kbd_bl_auto_trigger.name);
+ break;
+ }
+
+ priv->kbd_bl.led.flags |= LED_TRIG_HW_CHANGED;
+ priv->kbd_bl.led.hw_control_trigger = ideapad_kbd_bl_auto_trigger.name;
+ priv->kbd_bl.led.trigger_type = &ideapad_kbd_bl_auto_trigger_type;
+
+ /* Hardware remembers the last brightness level, including auto mode. */
+ if (hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS)
+ priv->kbd_bl.led.default_trigger = ideapad_kbd_bl_auto_trigger.name;
+
+ break;
+ case KBD_BL_TRISTATE:
+ priv->kbd_bl.led.max_brightness = 2;
+ break;
+ case KBD_BL_STANDARD:
+ priv->kbd_bl.led.max_brightness = 1;
+ break;
+ default:
+ /* This has already been validated by ideapad_check_features(). */
+ unreachable();
+ }
+
+ /* Queue notifications, as kbd_bl.initialized is about to be set. */
+ guard(mutex)(&priv->kbd_bl.notif_mutex);
+
+ /*
+ * Setting kbd_bl.initialized after led_classdev_register() could lead
+ * to race conditions in ideapad_kbd_bl_led_cdev_brightness_set() where
+ * kbd_bl.initialized is checked, so set it now. It can be reverted back
+ * if the LED classdev failed to register.
+ */
priv->kbd_bl.initialized = true;
- return 0;
+ err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led);
+ if (err)
+ priv->kbd_bl.initialized = false;
+
+ return err;
}
static void ideapad_kbd_bl_exit(struct ideapad_private *priv)
@@ -2548,17 +2690,30 @@ static int __init ideapad_laptop_init(void)
{
int err;
+ err = led_trigger_register(&ideapad_kbd_bl_auto_trigger);
+ if (err) {
+ pr_warn("Failed to register LED trigger %s: %d\n",
+ ideapad_kbd_bl_auto_trigger.name, err);
+ } else {
+ ideapad_kbd_bl_auto_trigger_registered = true;
+ }
+
err = ideapad_wmi_driver_register();
if (err)
- return err;
+ goto err_ledtrig;
err = platform_driver_register(&ideapad_acpi_driver);
- if (err) {
- ideapad_wmi_driver_unregister();
- return err;
- }
+ if (err)
+ goto err_wmi;
return 0;
+
+err_wmi:
+ ideapad_wmi_driver_unregister();
+err_ledtrig:
+ if (ideapad_kbd_bl_auto_trigger_registered)
+ led_trigger_unregister(&ideapad_kbd_bl_auto_trigger);
+ return err;
}
module_init(ideapad_laptop_init)
@@ -2566,6 +2721,9 @@ static void __exit ideapad_laptop_exit(void)
{
ideapad_wmi_driver_unregister();
platform_driver_unregister(&ideapad_acpi_driver);
+
+ if (ideapad_kbd_bl_auto_trigger_registered)
+ led_trigger_unregister(&ideapad_kbd_bl_auto_trigger);
}
module_exit(ideapad_laptop_exit)
diff --git a/include/linux/leds.h b/include/linux/leds.h
index b16b803cc1ac51..167598962b73ec 100644
--- a/include/linux/leds.h
+++ b/include/linux/leds.h
@@ -109,6 +109,7 @@ struct led_classdev {
#define LED_INIT_DEFAULT_TRIGGER BIT(23)
#define LED_REJECT_NAME_CONFLICT BIT(24)
#define LED_MULTI_COLOR BIT(25)
+#define LED_TRIG_HW_CHANGED BIT(26)
/* set_brightness_work / blink_timer flags, atomic, private. */
unsigned long work_flags;
@@ -485,6 +486,7 @@ struct led_trigger {
const char *name;
int (*activate)(struct led_classdev *led_cdev);
void (*deactivate)(struct led_classdev *led_cdev);
+ bool (*offloaded)(struct led_classdev *led_cdev);
/* Brightness set by led_trigger_event */
enum led_brightness brightness;
@@ -533,6 +535,9 @@ void led_trigger_blink_oneshot(struct led_trigger *trigger,
void led_trigger_set_default(struct led_classdev *led_cdev);
int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trigger);
void led_trigger_remove(struct led_classdev *led_cdev);
+void led_trigger_remove_hw_control(struct led_classdev *led_cdev);
+
+bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev);
static inline void led_set_trigger_data(struct led_classdev *led_cdev,
void *trigger_data)
@@ -584,6 +589,13 @@ static inline int led_trigger_set(struct led_classdev *led_cdev,
}
static inline void led_trigger_remove(struct led_classdev *led_cdev) {}
+static inline void led_trigger_remove_hw_control(struct led_classdev *led_cdev) {}
+
+static inline bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev)
+{
+ return false;
+}
+
static inline void led_set_trigger_data(struct led_classdev *led_cdev) {}
static inline void *led_get_trigger_data(struct led_classdev *led_cdev)
{
@@ -598,6 +610,13 @@ led_trigger_get_brightness(const struct led_trigger *trigger)
#endif /* CONFIG_LEDS_TRIGGERS */
+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED
+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate);
+#else
+static inline void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev,
+ bool activate) {}
+#endif
+
/* Trigger specific enum */
enum led_trigger_netdev_modes {
TRIGGER_NETDEV_LINK = 0,
|
| 3/2 | 2026/07/18 17:53 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 14456 output: 25 thoughts: 416 Thoughts: **Fuzzing Feasibility Analysis for LED Hardware Trigger Transitions** Okay, so I've got this patch series in front of me that aims to introduce support for hardware-initiated trigger transitions in the LED subsystem. My initial thought is, "Is this worth fuzzing?" Let's break it down. It touches a decent number of files, including the core LED classes (`led-class.c`, `led-triggers.c`), some specific platform drivers (`leds-cros_ec.c`, `leds-turris-omnia.c`, `ideapad-laptop.c`), and a network device trigger (`ledtrig-netdev.c`). That's a reasonable footprint, which gives us a solid attack surface to think about. The changes involve adding new functions to the core trigger logic: `led_trigger_notify_hw_control_changed`, `led_trigger_is_hw_controlled`, and `led_trigger_remove_hw_control`. Also, `ideapad-laptop.c` is being modified to actually *use* this new feature. Right, so this core LED trigger logic is definitely accessible through sysfs. That's a major plus. The platform driver is a bit of a question mark - might be tricky to test without specific hardware, but the core stuff is fair game. Hang on... Can we actually reach `led_trigger_notify_hw_control_changed` directly from userspace? Hmm, no, looks like drivers call it when hardware state changes. But we *can* get to `trigger_may_offload_show` via sysfs, and `led_trigger_set` via the `trigger` attribute. Now, `led_trigger_remove_hw_control`...that's called from `brightness_store` when the LED state isn't `LED_OFF`. Interesting. Let's dig a little deeper. Where else is `led_trigger_notify_hw_control_changed` called? Looks like `ideapad_kbd_bl_notify_hw_control` in `ideapad-laptop.c`. Let's confirm with a quick `grep` just to be sure there are no other call sites, and we can confirm that we understand the relevant attack surface. |
| 4/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "led_trigger_notify_hw_control_changed"
}Results: {
"Output": "Documentation/leds/leds-class.rst=345=flags before registering. To update the current trigger accordingly, call\nDocumentation/leds/leds-class.rst:346:`led_trigger_notify_hw_control_changed` on the LED classdev.\nDocumentation/leds/leds-class.rst-347-\n--\ndrivers/leds/led-triggers.c=492=static void led_trigger_do_hw_control_transition(struct led_classdev *led_cdev, bool activate,\n--\ndrivers/leds/led-triggers.c-519-\ndrivers/leds/led-triggers.c:520:void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)\ndrivers/leds/led-triggers.c-521-{\n--\ndrivers/leds/led-triggers.c-544-}\ndrivers/leds/led-triggers.c:545:EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed);\ndrivers/leds/led-triggers.c-546-#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */\n--\ndrivers/platform/x86/lenovo/ideapad-laptop.c=1754=static void ideapad_kbd_bl_notify_hw_control(struct ideapad_private *priv,\n--\ndrivers/platform/x86/lenovo/ideapad-laptop.c-1765-\tif (hw_control != last_hw_control)\ndrivers/platform/x86/lenovo/ideapad-laptop.c:1766:\t\tled_trigger_notify_hw_control_changed(\u0026priv-\u003ekbd_bl.led, hw_control);\ndrivers/platform/x86/lenovo/ideapad-laptop.c-1767-}\n--\ninclude/linux/leds.h=606=led_trigger_get_brightness(const struct led_trigger *trigger)\n--\ninclude/linux/leds.h-613-#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\ninclude/linux/leds.h:614:void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate);\ninclude/linux/leds.h-615-#else\ninclude/linux/leds.h:616:static inline void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev,\ninclude/linux/leds.h-617-\t\t\t\t\t\t\t bool activate) {}\n"
} |
| 5/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 16012 output: 25 thoughts: 86 Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience: **Investigating LED Trigger Configuration** Alright, so the core of managing those LED triggers should be accessible via sysfs. The next logical step is to pinpoint the exact kernel configurations required to enable and utilize them effectively. Let's see... I'll start with the obvious: `CONFIG_LEDS_TRIGGERS`. That's the baseline, no doubt. Then, I suspect we'll need `CONFIG_LEDS_TRIGGERS_HW_CHANGED` for specific hardware-related changes as the name implies, and of course, `CONFIG_LEDS_CLASS` is probably also critical for the device class support. To get a definitive answer on the `HW_CHANGED` trigger's configuration options, I'll go straight to the source. Specifically, I should check the `drivers/leds/trigger/Kconfig` file. That's where all the details for that particular trigger should reside. This is the fastest route to confirm dependencies and understand any nuances in its behavior. |
| 6/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "config LEDS_TRIGGERS_HW_CHANGED"
}Results: {
"Output": "drivers/leds/trigger/Kconfig=10=if LEDS_TRIGGERS\ndrivers/leds/trigger/Kconfig-11-\ndrivers/leds/trigger/Kconfig:12:config LEDS_TRIGGERS_HW_CHANGED\ndrivers/leds/trigger/Kconfig-13-\tbool \"LED hardware-initiated trigger transition support\"\n"
} |
| 7/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 16447 output: 24 thoughts: 133 Thoughts: **My Assessment of the LED Trigger Configuration** Okay, first things first, is this `LEDS_TRIGGERS_HW_CHANGED` setting enabled by default? No, it's a bool, so that answers that question. I need to make sure this gets added to our EnableConfigs. That's a key requirement. Now, what about the `ideapad-laptop.c` driver? Ah, that relies on `CONFIG_IDEAPAD_LAPTOP`. That's an x86 platform driver, tied to ACPI and specific hardware, so fuzzing it directly could be tricky. It's not going to be the *easiest* target, that's for sure. I am not seeing a direct path for easier fuzzing of this driver. However, the core LED trigger logic, that's still potentially reachable. We might still have a shot, even if it is not the easiest. Let me see what's going on in `drivers/leds/led-triggers.c`. That's where the heart of the matter likely lies. |
| 8/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "__led_trigger_is_hw_controlled"
}Results: {
"Output": "drivers/leds/led-triggers.c=34=trigger_relevant(struct led_classdev *led_cdev, struct led_trigger *trig)\n--\ndrivers/leds/led-triggers.c-38-\ndrivers/leds/led-triggers.c:39:static bool __led_trigger_is_hw_controlled(struct led_classdev *led_cdev)\ndrivers/leds/led-triggers.c-40-{\n--\ndrivers/leds/led-triggers.c=59=bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev)\n--\ndrivers/leds/led-triggers.c-61-\tguard(rwsem_read)(\u0026led_cdev-\u003etrigger_lock);\ndrivers/leds/led-triggers.c:62:\treturn __led_trigger_is_hw_controlled(led_cdev);\ndrivers/leds/led-triggers.c-63-}\n--\ndrivers/leds/led-triggers.c=321=void led_trigger_remove_hw_control(struct led_classdev *led_cdev)\n--\ndrivers/leds/led-triggers.c-324-\ndrivers/leds/led-triggers.c:325:\tif (__led_trigger_is_hw_controlled(led_cdev))\ndrivers/leds/led-triggers.c-326-\t\tled_trigger_set(led_cdev, NULL);\n--\ndrivers/leds/led-triggers.c=375=ssize_t trigger_may_offload_show(struct device *dev,\n--\ndrivers/leds/led-triggers.c-387-\ndrivers/leds/led-triggers.c:388:\toffloaded = __led_trigger_is_hw_controlled(led_cdev);\ndrivers/leds/led-triggers.c-389-\thit = offloaded || (trig \u0026\u0026 !strcmp(led_cdev-\u003ehw_control_trigger, trig-\u003ename));\n"
} |
| 9/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 17330 output: 25 thoughts: 109 Thoughts: **Focusing on Hardware LED Trigger Management** Okay, so I'm looking at this LED trigger code, and I need to get a handle on the key functions. Let's see... the ones I need to zero in on are: `__led_trigger_is_hw_controlled`, `led_trigger_is_hw_controlled`, `led_trigger_remove_hw_control`, `trigger_may_offload_show`, `led_trigger_do_hw_control_transition`, `led_trigger_notify_hw_control_changed`, and `__led_trigger_set`. Right, that seems like a good core set to start with. The interaction between hardware control and the software triggers is important. I think that `led_trigger_do_hw_control_transition` is probably the most critical place to begin understanding the state transitions. Let's see what that function is doing. |
| 10/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "led_trigger_do_hw_control_transition"
}Results: {
"Output": "drivers/leds/led-triggers.c=489=EXPORT_SYMBOL_GPL(devm_led_trigger_register);\n--\ndrivers/leds/led-triggers.c-491-#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\ndrivers/leds/led-triggers.c:492:static void led_trigger_do_hw_control_transition(struct led_classdev *led_cdev, bool activate,\ndrivers/leds/led-triggers.c-493-\t\t\t\t\t\t struct led_trigger *hc_trig)\n--\ndrivers/leds/led-triggers.c=520=void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)\n--\ndrivers/leds/led-triggers.c-534-\ndrivers/leds/led-triggers.c:535:\t\t\t\tled_trigger_do_hw_control_transition(led_cdev, activate, trig);\ndrivers/leds/led-triggers.c-536-\t\t\t\treturn;\n"
} |
| 11/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 17925 output: 23 thoughts: 151 Thoughts: **Exploring LED Trigger Control** Okay, so I'm trying to figure out if I can manipulate `led_trigger_notify_hw_control_changed` from userspace, specifically to trigger some interesting behavior. My initial thought is no, because it appears to be primarily called by drivers. But then... hold on, it *is* exported! That opens a tiny window. The question is, can I find a vulnerable driver that provides a backdoor? My next avenue of attack is to find a fuzzable driver that *might* be reachable. `ideapad-laptop.c` is tempting. It's an ACPI driver, and ACPI interfaces can, in theory, be fuzz tested. It's not a direct route, I know, ACPI fuzzing can be a pain in the... well, you get the idea. It's not the easiest path. Then I shifted my perspective, what about a similar function `led_trigger_remove_hw_control`? Where is that called? I find it in `brightness_store` within `led-class.c`. Ah, that’s interesting, `brightness_store` is accessible through sysfs. That's a direct route into the kernel from userspace. I've got a much more accessible interface there now. |
| 12/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "led_trigger_remove_hw_control"
}Results: {
"Output": "drivers/leds/led-class.c=47=static ssize_t brightness_store(struct device *dev,\n--\ndrivers/leds/led-class.c-67-\telse\ndrivers/leds/led-class.c:68:\t\tled_trigger_remove_hw_control(led_cdev);\ndrivers/leds/led-class.c-69-\n--\ndrivers/leds/led-triggers.c=319=EXPORT_SYMBOL_GPL(led_trigger_remove);\ndrivers/leds/led-triggers.c-320-\ndrivers/leds/led-triggers.c:321:void led_trigger_remove_hw_control(struct led_classdev *led_cdev)\ndrivers/leds/led-triggers.c-322-{\n--\ndrivers/leds/led-triggers.c-327-}\ndrivers/leds/led-triggers.c:328:EXPORT_SYMBOL_GPL(led_trigger_remove_hw_control);\ndrivers/leds/led-triggers.c-329-\n--\ninclude/linux/leds.h=537=void led_trigger_remove(struct led_classdev *led_cdev);\ninclude/linux/leds.h:538:void led_trigger_remove_hw_control(struct led_classdev *led_cdev);\ninclude/linux/leds.h-539-\n--\ninclude/linux/leds.h=591=static inline void led_trigger_remove(struct led_classdev *led_cdev) {}\ninclude/linux/leds.h:592:static inline void led_trigger_remove_hw_control(struct led_classdev *led_cdev) {}\ninclude/linux/leds.h-593-\n"
} |
| 13/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 18819 output: 19 thoughts: 127 Thoughts: **Analyzing LED Trigger Control Flow** Okay, so I'm looking at this code and trying to map out the control flow for these LED triggers. It seems that `led_trigger_remove_hw_control` is definitely reachable. Good, that's a key function to understand. Then, the `trigger_may_offload_show` function, which deals with the sysfs interface through `trigger_may_offload`, is also definitely reachable. Now, that's interesting because `__led_trigger_is_hw_controlled` has two different paths to it, through the calls to `trigger_may_offload_show` and `led_trigger_remove_hw_control`. That's important to keep in mind, and the connection makes sense. And then finally, there's `__led_trigger_set`, that's reachable through `led_trigger_set`. This last one is ultimately called from `led_trigger_write`, which itself is associated with the sysfs `trigger` attribute. So, if a user writes to that sysfs entry, we're ultimately hitting `__led_trigger_set`. Alright, I think I've got a decent grasp on this. Now let's dig into `led_trigger_write`. That's where the magic really seems to be happening with the user-space interaction. |
| 14/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "led_trigger_write"
}Results: {
"Output": "drivers/leds/led-class.c=91=static DEVICE_ATTR_RO(max_brightness);\n--\ndrivers/leds/led-class.c-93-#ifdef CONFIG_LEDS_TRIGGERS\ndrivers/leds/led-class.c:94:static const BIN_ATTR(trigger, 0644, led_trigger_read, led_trigger_write, 0);\ndrivers/leds/led-class.c-95-static const struct bin_attribute *const led_trigger_bin_attrs[] = {\n--\ndrivers/leds/led-triggers.c=64=EXPORT_SYMBOL_GPL(led_trigger_is_hw_controlled);\ndrivers/leds/led-triggers.c-65-\ndrivers/leds/led-triggers.c:66:ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,\ndrivers/leds/led-triggers.c-67-\t\t\t const struct bin_attribute *bin_attr, char *buf,\n--\ndrivers/leds/led-triggers.c-110-}\ndrivers/leds/led-triggers.c:111:EXPORT_SYMBOL_GPL(led_trigger_write);\ndrivers/leds/led-triggers.c-112-\n--\ndrivers/leds/leds.h=24=ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,\n--\ndrivers/leds/leds.h-26-\t\t\tloff_t pos, size_t count);\ndrivers/leds/leds.h:27:ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,\ndrivers/leds/leds.h-28-\t\t\tconst struct bin_attribute *bin_attr, char *buf,\n"
} |
| 15/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 19709 output: 21 thoughts: 420 Thoughts: **Fuzzing the LED Trigger Logic** Okay, so I've pinpointed the core LED trigger logic as a promising target for fuzzing. Definitely a good place to start digging in. The key configurations I need to enable are `LEDS_CLASS`, `LEDS_TRIGGERS`, and `LEDS_TRIGGERS_HW_CHANGED`. That last one is particularly interesting as it handles hardware-initiated trigger transitions. Wait, I see that `ideapad-laptop.c` is also modified, which makes me wonder if that driver is also worth fuzzing. It's a platform driver, though, and Syzkaller isn't typically great at fuzzing ACPI platform drivers unless there's specific support. But that core LED trigger logic is more generic, so I'll prioritize that for now. I'll focus on `LEDS_TRIGGERS_HW_CHANGED` to ensure hardware-initiated transition support. Now, which functions are the most important to focus on? Let's see: `__led_trigger_is_hw_controlled`, `led_trigger_is_hw_controlled`, `__led_trigger_set`, `led_trigger_remove_hw_control`, `trigger_may_offload_show`, `led_trigger_do_hw_control_transition`, and `led_trigger_notify_hw_control_changed`. These look like the main entry points and control flow functions. Okay, I need to know if `led_trigger_do_hw_control_transition` is reachable without `ideapad-laptop.c`. It's only called by `led_trigger_notify_hw_control_changed`, which is only called by `ideapad-laptop.c`. Hmm, so if I want to fuzz `led_trigger_notify_hw_control_changed`, I need `ideapad-laptop.c` or another driver that calls it. Since Syzkaller can't easily reach `ideapad-laptop.c` due to its ACPI hardware dependency, maybe I should try enabling `IDEAPAD_LAPTOP` in the configs just in case. It might be worth including, right? Let me check `drivers/platform/x86/lenovo/Kconfig` to see how it's defined. |
| 16/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "config IDEAPAD_LAPTOP"
}Results: {
"Output": "drivers/platform/x86/lenovo/Kconfig-5-\ndrivers/platform/x86/lenovo/Kconfig:6:config IDEAPAD_LAPTOP\ndrivers/platform/x86/lenovo/Kconfig-7-\ttristate \"Lenovo IdeaPad Laptop Extras\"\n"
} |
| 17/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 20720 output: 22 thoughts: 46 Thoughts: **My Immediate Plan** Okay, so this is definitely contingent on `ACPI` and the underlying `X86` architecture. That's the foundation we're working with here. I need to make sure `IDEAPAD_LAPTOP` is included within the `EnableConfigs` list to properly enable the system. Now, my next step is to dig into the `leds-cros_ec.c` file. I'm going to start there to see how the EC handles the LEDs, and hopefully, I can trace the control flow from there. |
| 18/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "config LEDS_CROS_EC"
}Results: {
"Output": "drivers/leds/Kconfig=189=config LEDS_CR0014114\n--\ndrivers/leds/Kconfig-201-\ndrivers/leds/Kconfig:202:config LEDS_CROS_EC\ndrivers/leds/Kconfig-203-\ttristate \"LED Support for ChromeOS EC\"\n"
} |
| 19/2 | 2026/07/18 17:54 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 21007 output: 23 thoughts: 12 Thoughts: Okay, here's my interpretation of that thought, summarized in the first person, assuming I'm a moderately experienced developer familiar with embedded systems: **Considering `leds-turris-omnia.c`** Right, so that thought immediately makes me think about driver implementations. I'm likely in the context of some low-level hardware interaction, probably within the Linux kernel or something very closely related. "leds" obviously points to LED control, and the "turris-omnia" part is the specific target platform – a Turris Omnia router, I believe. So I'm probably looking at the code responsible for managing the LEDs on that particular device. I'm probably thinking about how these LEDs are mapped to specific hardware pins, the control registers involved, and how the driver provides the necessary interfaces for higher-level applications (like the user interface) to interact with them. I'll likely be going through the code looking at the functions for setting LED states (on/off, blinking, brightness), interrupt handling (if any), and how these functions interact with the device's hardware. I might be considering modifications or debugging a problem with the driver. I'll likely be checking for correct interrupt handling, DMA, concurrency, and memory management. Hopefully, the code is well-structured and follows good coding practices. |
| 20/2 | 2026/07/18 17:54 | tool | grepper |
0mArgs:{
"Expression": "config LEDS_TURRIS_OMNIA"
}Results: {
"Output": "drivers/leds/Kconfig=217=config LEDS_EL15203000\n--\ndrivers/leds/Kconfig-229-\ndrivers/leds/Kconfig:230:config LEDS_TURRIS_OMNIA\ndrivers/leds/Kconfig-231-\ttristate \"LED support for CZ.NIC's Turris Omnia\"\n"
} |
| 21/2 | 2026/07/18 17:55 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 21423 output: 22 thoughts: 11 Thoughts: Okay, here's my summary, presented as a stream of thought: **Diving into the Code: `ledtrig-netdev.c`** Alright, let's see what's what. I'm focusing on that `ledtrig-netdev.c` file again. Seems like a critical piece, doesn't it? It's where the network device LED triggering logic resides, obviously. The "ledtrig" prefix gives it away. I'm imagining how this interacts with the kernel's network stack. My experience has been I have seen similar constructs handling interface changes, packet drops, or link status changes. I need to refresh my mind on the general LED triggering framework. What are the key data structures here? How are the network events hooked into the LED driver calls? I bet there's some device registration and unregistration going on, dealing with Netlink events, probably some workqueues to avoid blocking. I need to identify the data flow and how the netdev events, interface up, interface down, transmit or receive traffic events are fed into this and ultimately how they change an LED status. I anticipate seeing some clever tricks to keep the overhead low, especially in high-bandwidth scenarios. I should probably focus on the interrupt context versus task context. I'm going to dig into the interrupt path first; get the most critical part of the data flow. |
| 22/2 | 2026/07/18 17:55 | tool | grepper |
0mArgs:{
"Expression": "config LEDS_TRIGGER_NETDEV"
}Results: {
"Output": "drivers/leds/trigger/Kconfig=125=config LEDS_TRIGGER_PANIC\n--\ndrivers/leds/trigger/Kconfig-133-\ndrivers/leds/trigger/Kconfig:134:config LEDS_TRIGGER_NETDEV\ndrivers/leds/trigger/Kconfig-135-\ttristate \"LED Netdev Trigger\"\n"
} |
| 23/2 | 2026/07/18 17:55 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 21847 output: 227 thoughts: 1132 Thoughts: **Fuzzing Strategy for LED Hardware Control and Offload** Okay, so I'm thinking we need to add `LEDS_TRIGGER_NETDEV` to the `EnableConfigs`. It's a fairly generic trigger that implements `offloaded`, and that makes it a good starting point for testing this new functionality. I'm also going to throw in `LEDS_TRIGGERS_HW_CHANGED` because that's the core of the changes. I'll keep `IDEAPAD_LAPTOP` as well, since that's another important one. I briefly considered adding `LEDS_CLASS`, `LEDS_TRIGGERS`, `LEDS_CROS_EC` and `LEDS_TURRIS_OMNIA` as well, but `LEDS_CLASS` and `LEDS_TRIGGERS` are probably already enabled by default. `LEDS_CROS_EC` depends on `CROS_EC`, and `LEDS_TURRIS_OMNIA` has more dependencies, so `LEDS_TRIGGER_NETDEV` along with `LEDS_TRIGGERS_HW_CHANGED` seems like a good balance for the moment. Let's see if that's enough to get at the core issue. Then there's the `FocusSymbols` list. I've gone through and confirmed that static functions are supported by syzkaller. I'll need to include: * `__led_trigger_is_hw_controlled` * `led_trigger_is_hw_controlled` * `led_trigger_remove_hw_control` * `trigger_may_offload_show` * `led_trigger_do_hw_control_transition` * `led_trigger_notify_hw_control_changed` * `__led_trigger_set` * `netdev_trig_offloaded` I looked at the code to see where these get used. `led_trigger_remove_hw_control` is called when writing to `/sys/class/leds/<led>/brightness` when the state isn't LED_OFF, which means the fuzzing will trigger the code path. `trigger_may_offload_show` is called on reading `/sys/class/leds/<led>/trigger_may_offload`, which is another important sysfs interaction. `led_trigger_notify_hw_control_changed` is called by drivers and triggers a state transition, important for reaching the changed logic. Ultimately, these changes introduce new functionality to the LED subsystem, specifically hardware-initiated trigger transitions and offload state reporting. They're accessible via sysfs interactions and driver notifications. I'm setting `WorthFuzzing` to `true` because this patch introduces significant new code paths related to hardware control and offload, which are likely to have security implications. |
| 24/2 | 2026/07/18 17:55 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"__led_trigger_is_hw_controlled",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"trigger_may_offload_show",
"led_trigger_do_hw_control_transition",
"led_trigger_notify_hw_control_changed",
"__led_trigger_set",
"netdev_trig_offloaded"
],
"Reasoning": "The patch introduces functional changes to the core LED trigger subsystem, adding support for hardware-initiated trigger transitions and offload state reporting. These new code paths are reachable via sysfs attributes (`trigger_may_offload`, `brightness`, `trigger`) and driver notifications. Fuzzing should focus on the new state transition logic and offload queries.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"__led_trigger_is_hw_controlled",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"trigger_may_offload_show",
"led_trigger_do_hw_control_transition",
"led_trigger_notify_hw_control_changed",
"__led_trigger_set",
"netdev_trig_offloaded"
],
"Reasoning": "The patch introduces functional changes to the core LED trigger subsystem, adding support for hardware-initiated trigger transitions and offload state reporting. These new code paths are reachable via sysfs attributes (`trigger_may_offload`, `brightness`, `trigger`) and driver notifications. Fuzzing should focus on the new state transition logic and offload queries.",
"WorthFuzzing": true
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|