| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/09/02 07:22 | flow | patch-triage |
3mArgs:null Results: {
"EnableConfigs": [
"NEW_LEDS",
"LEDS_CLASS",
"LEDS_TRIGGERS",
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"LEDS_BRIGHTNESS_HW_CHANGED",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"trigger_may_offload_show",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"led_trigger_set",
"led_trigger_notify_hw_control_changed"
],
"KMSANReasoning": "The patch adds hardware-controlled LED trigger offload infrastructure, the `trigger_may_offload` sysfs attribute, hardware trigger transition notifications via a workqueue, and updates drivers including ideapad-laptop and leds-cros_ec.\n\n1. No data structures are copied to userspace via copy_to_user, netlink, ioctl, or socket buffers with uninitialized padding or fields. Userspace exposure is limited to standard sysfs_emit string formatting.\n2. All local variables and struct fields (e.g., triggers_hw_changed, last_hw_brightness, hw_brightness) are explicitly initialized before use or branching.\n3. Concurrency and lifecycle handling (such as workqueue flush/destruction and mutex/rwsem locks) fall under the purview of LOCKDEP and standard KASAN rather than KMSAN.\n\nThere are no uninitialized memory access or info-leak risks that would require dedicated KMSAN fuzzing.",
"NeedsKMSAN": false,
"Reasoning": "The patch series introduces a generic sysfs attribute (trigger_may_offload) for queryable hardware-offloaded LED triggers, updates core brightness get/set locking and trigger removal logic, and implements support for hardware-initiated trigger transitions (CONFIG_LEDS_TRIGGERS_HW_CHANGED) along with updates to LED trigger drivers. These changes modify reachable LED class/trigger sysfs interfaces and core trigger lifecycle handling.",
"WorthFuzzing": true
} |
| 1/1 | 2026/09/02 07:22 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit d07a5e3c92114c75453172a9452ab7a25bc34b52\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Wed Sep 2 07:22:27 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 d4c918cc11a12..94c331d9c4105 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\tSeptember 2026\n+KernelVersion:\t7.4\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 ed46b37ab8a28..a5146ea1e3e61 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 3913966cfdac0..adbc57b9f49c3 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 a51b0ed538864..3b438d8da5e0c 100644\n--- a/drivers/leds/led-class.c\n+++ b/drivers/leds/led-class.c\n@@ -27,29 +27,19 @@ 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 \tstruct led_classdev *led_cdev = dev_get_drvdata(dev);\n \tunsigned int brightness;\n \n-\tif (led_trigger_is_hw_controlled(led_cdev))\n-\t\treturn -ENODATA;\n+\tscoped_guard(mutex, \u0026led_cdev-\u003eled_access) {\n+\t\tif (led_trigger_is_hw_controlled(led_cdev))\n+\t\t\treturn -ENODATA;\n \n-\tmutex_lock(\u0026led_cdev-\u003eled_access);\n-\tled_update_brightness(led_cdev);\n-\tbrightness = led_cdev-\u003ebrightness;\n-\tmutex_unlock(\u0026led_cdev-\u003eled_access);\n+\t\tled_update_brightness(led_cdev);\n+\t\tbrightness = led_cdev-\u003ebrightness;\n+\t}\n \n \treturn sysfs_emit(buf, \"%u\\n\", brightness);\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 \n@@ -596,6 +611,9 @@ int led_classdev_register_ext(struct device *parent,\n \tled_trigger_set_default(led_cdev);\n #endif\n \n+\tif (led_cdev-\u003eflags \u0026 LED_TRIG_HW_CHANGED)\n+\t\tled_trigger_init_hw_changed(led_cdev);\n+\n \tmutex_unlock(\u0026led_cdev-\u003eled_access);\n \n \tdev_dbg(parent, \"Registered led device: %s\\n\",\n@@ -616,6 +634,9 @@ void led_classdev_unregister(struct led_classdev *led_cdev)\n \tif (IS_ERR_OR_NULL(led_cdev-\u003edev))\n \t\treturn;\n \n+\tif (led_cdev-\u003eflags \u0026 LED_TRIG_HW_CHANGED)\n+\t\tled_trigger_destroy_hw_changed(led_cdev);\n+\n #ifdef CONFIG_LEDS_TRIGGERS\n \tdown_write(\u0026led_cdev-\u003etrigger_lock);\n \tif (led_cdev-\u003etrigger)\ndiff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c\nindex b1223218bda11..a9d992a886161 100644\n--- a/drivers/leds/led-triggers.c\n+++ b/drivers/leds/led-triggers.c\n@@ -7,9 +7,13 @@\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/compiler.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 +37,34 @@ 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+\t/* Otherwise assume private triggers as always offloaded. */\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 +195,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 +227,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 +305,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 +320,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 +374,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 +490,88 @@ 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+\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+static void led_trigger_hw_control_changed_worker(struct work_struct *work)\n+{\n+\tstruct led_classdev *led_cdev =\n+\t\tcontainer_of(work, struct led_classdev, triggers_hw_changed_work);\n+\tbool activate = READ_ONCE(led_cdev-\u003etriggers_hw_changed);\n+\n+\tscoped_guard(rwsem_read, \u0026triggers_list_lock) {\n+\t\tstruct led_trigger *trig;\n+\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+\n+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)\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+\tWRITE_ONCE(led_cdev-\u003etriggers_hw_changed, activate);\n+\n+\tschedule_work(\u0026led_cdev-\u003etriggers_hw_changed_work);\n+}\n+EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed);\n+\n+void led_trigger_init_hw_changed(struct led_classdev *led_cdev)\n+{\n+\tINIT_WORK(\u0026led_cdev-\u003etriggers_hw_changed_work, led_trigger_hw_control_changed_worker);\n+}\n+EXPORT_SYMBOL_GPL(led_trigger_init_hw_changed);\n+\n+void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev)\n+{\n+\tdisable_work_sync(\u0026led_cdev-\u003etriggers_hw_changed_work);\n+}\n+EXPORT_SYMBOL_GPL(led_trigger_destroy_hw_changed);\n+\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 1844d0cd5f528..6db83d0152775 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 ed6a47bbb44f9..32d40d176d3f1 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 bee46651e068f..bdac2336012e6 100644\n--- a/drivers/leds/leds.h\n+++ b/drivers/leds/leds.h\n@@ -27,8 +27,18 @@ 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;\n \n+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\n+void led_trigger_init_hw_changed(struct led_classdev *led_cdev);\n+void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev);\n+#else /* !CONFIG_LEDS_TRIGGERS_HW_CHANGED */\n+static inline void led_trigger_init_hw_changed(struct led_classdev *led_cdev) { }\n+static inline void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev) { }\n+#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */\n+\n #endif\t/* __LEDS_H_INCLUDED */\ndiff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig\nindex c11282a74b5ac..a11d04ce4ab2c 100644\n--- a/drivers/leds/trigger/Kconfig\n+++ b/drivers/leds/trigger/Kconfig\n@@ -9,6 +9,16 @@ menuconfig LEDS_TRIGGERS\n \n if LEDS_TRIGGERS\n \n+config LEDS_TRIGGERS_HW_CHANGED\n+\tbool \"LED hardware-initiated trigger transition support\"\n+\tdefault LEDS_BRIGHTNESS_HW_CHANGED\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 5b4e92c14dbb4..2e9af48a74dbf 100644\n--- a/drivers/leds/trigger/ledtrig-netdev.c\n+++ b/drivers/leds/trigger/ledtrig-netdev.c\n@@ -798,10 +798,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 5165949930739..a97852b0b2284 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 8213524504eeb..f86c11f51d0be 100644\n--- a/drivers/platform/x86/lenovo/ideapad-laptop.c\n+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c\n@@ -14,9 +14,12 @@\n #include \u003clinux/bitops.h\u003e\n #include \u003clinux/bug.h\u003e\n #include \u003clinux/cleanup.h\u003e\n+#include \u003clinux/compiler.h\u003e\n+#include \u003clinux/container_of.h\u003e\n #include \u003clinux/debugfs.h\u003e\n #include \u003clinux/delay.h\u003e\n #include \u003clinux/device.h\u003e\n+#include \u003clinux/dev_printk.h\u003e\n #include \u003clinux/dmi.h\u003e\n #include \u003clinux/i8042.h\u003e\n #include \u003clinux/init.h\u003e\n@@ -26,9 +29,11 @@\n #include \u003clinux/kernel.h\u003e\n #include \u003clinux/leds.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+#include \u003clinux/printk.h\u003e\n #include \u003clinux/rfkill.h\u003e\n #include \u003clinux/seq_file.h\u003e\n #include \u003clinux/string_choices.h\u003e\n@@ -134,10 +139,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 +171,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@@ -200,10 +228,14 @@ struct ideapad_private {\n \t\tbool ymc_ec_trigger : 1;\n \t} features;\n \tstruct {\n-\t\tbool initialized;\n+\t\tint initialized; /* 0: initializion pending/ongoing/failed\n+\t\t\t\t * \u003e0: initializion finished\n+\t\t\t\t * \u003c0: unregisteration ongoing/finished\n+\t\t\t\t */\n \t\tint type;\n \t\tstruct led_classdev led;\n-\t\tunsigned int last_brightness;\n+\t\tunsigned int last_hw_brightness;\n+\t\tstruct mutex mutex; /* protects brightness tracking */\n \t} kbd_bl;\n \tstruct {\n \t\tbool initialized;\n@@ -1592,7 +1624,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, unsigned 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 +1655,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 +1665,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,99 +1682,207 @@ 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,\n+\t\t\t\t\t unsigned 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+\tguard(mutex)(\u0026priv-\u003ekbd_bl.mutex);\n \n-\t\tvalue = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) |\n+\tif (ideapad_kbd_bl_check_tristate(type)) {\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+\tpriv-\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, unsigned 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 unregistering: It must be the side effect of LED unregistration\n+\t * when our private trigger is active. We've set LED_RETAIN_AT_SHUTDOWN\n+\t * to retain led_cdev brightness level. To do the same for auto mode,\n+\t * gate changes and return early.\n+\t *\n+\t * It's needless to gate changes when initializing, as there may be a\n+\t * time margin between sysfs attribute creation and state change.\n+\t */\n+\tif (unlikely(priv-\u003ekbd_bl.initialized \u003c 0))\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+\tguard(mutex)(\u0026priv-\u003ekbd_bl.mutex);\n+\n+\treturn priv-\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 unsigned int hw_brightness)\n+{\n+\tbool hw_control, last_hw_control;\n+\n+\tif (!ideapad_kbd_bl_auto_trigger_registered || 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 = priv-\u003ekbd_bl.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;\n \n-\tif (!priv-\u003ekbd_bl.initialized)\n+\tif (unlikely(priv-\u003ekbd_bl.initialized \u003c= 0))\n \t\treturn;\n \n-\tbrightness = ideapad_kbd_bl_brightness_get(priv);\n-\tif (brightness \u003c 0)\n+\tguard(mutex)(\u0026priv-\u003ekbd_bl.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+\tif (priv-\u003ekbd_bl.last_hw_brightness == hw_brightness)\n \t\treturn;\n \n-\tpriv-\u003ekbd_bl.last_brightness = brightness;\n+\tideapad_kbd_bl_notify_hw_control(priv, hw_brightness);\n+\n+\tpriv-\u003ekbd_bl.last_hw_brightness = 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 \n-\tif (WARN_ON(priv-\u003ekbd_bl.initialized))\n+\tif (WARN_ON(priv-\u003ekbd_bl.initialized \u003e 0))\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+\tpriv-\u003ekbd_bl.initialized = 0;\n \n-\tbrightness = ideapad_kbd_bl_brightness_get(priv);\n-\tif (brightness \u003c 0)\n-\t\treturn brightness;\n+\terr = devm_mutex_init(\u0026priv-\u003eplatform_device-\u003edev, \u0026priv-\u003ekbd_bl.mutex);\n+\tif (err)\n+\t\treturn err;\n+\n+\thw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);\n+\tif (hw_brightness \u003c 0)\n+\t\treturn hw_brightness;\n+\n+\tpriv-\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+\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 \"Cannot 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.trigger_type = \u0026ideapad_kbd_bl_auto_trigger_type;\n+\t\tpriv-\u003ekbd_bl.led.hw_control_trigger = ideapad_kbd_bl_auto_trigger.name;\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/* Reject insane values. */\n+\terr = ideapad_kbd_bl_brightness_parse(priv, hw_brightness);\n+\tif (err \u003c 0)\n+\t\treturn err;\n+\n \terr = led_classdev_register(\u0026priv-\u003eplatform_device-\u003edev, \u0026priv-\u003ekbd_bl.led);\n \tif (err)\n \t\treturn err;\n \n-\tpriv-\u003ekbd_bl.initialized = true;\n+\tpriv-\u003ekbd_bl.initialized = 1;\n \n \treturn 0;\n }\n \n static void ideapad_kbd_bl_exit(struct ideapad_private *priv)\n {\n-\tif (!priv-\u003ekbd_bl.initialized)\n+\tif (priv-\u003ekbd_bl.initialized \u003c= 0)\n \t\treturn;\n \n-\tpriv-\u003ekbd_bl.initialized = false;\n+\tpriv-\u003ekbd_bl.initialized = -1;\n \n \tled_classdev_unregister(\u0026priv-\u003ekbd_bl.led);\n }\n@@ -2548,17 +2701,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 +2732,9 @@ static void __exit ideapad_laptop_exit(void)\n {\n \tplatform_driver_unregister(\u0026ideapad_acpi_driver);\n \tideapad_wmi_driver_unregister();\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 a515f075c29a1..93b3fd7e56364 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@@ -239,6 +240,11 @@ struct led_classdev {\n \tstruct kernfs_node\t*brightness_hw_changed_kn;\n #endif\n \n+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\n+\tbool\t\t\ttriggers_hw_changed;\n+\tstruct work_struct\ttriggers_hw_changed_work;\n+#endif\n+\n \t/* Ensures consistent access to the LED class device */\n \tstruct mutex\t\tled_access;\n };\n@@ -485,6 +491,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 +540,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 +594,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 +615,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/09/02 07:22 | agent | patch-evaluator |
2mModel:core Args: null Results: {
"EnableConfigs": [
"NEW_LEDS",
"LEDS_CLASS",
"LEDS_TRIGGERS",
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"LEDS_BRIGHTNESS_HW_CHANGED",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"trigger_may_offload_show",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"led_trigger_set",
"led_trigger_notify_hw_control_changed"
],
"Reasoning": "The patch series introduces a generic sysfs attribute (trigger_may_offload) for queryable hardware-offloaded LED triggers, updates core brightness get/set locking and trigger removal logic, and implements support for hardware-initiated trigger transitions (CONFIG_LEDS_TRIGGERS_HW_CHANGED) along with updates to LED trigger drivers. These changes modify reachable LED class/trigger sysfs interfaces and core trigger lifecycle handling.",
"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). - Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed, ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU. - Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove) that are executed only during PCI hot-unplug or sysfs driver unbind operations. 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. Prefer non-static, non-inlined API entrypoint functions over internal static helper functions (which are inlined by the compiler and do not have distinct symbol addresses). 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. Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed for Kbuild to compile the code statically into vmlinux. 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 d07a5e3c92114c75453172a9452ab7a25bc34b52
Author: syz-cluster <triage@syzkaller.com>
Date: Wed Sep 2 07:22:27 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 d4c918cc11a12..94c331d9c4105 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: September 2026
+KernelVersion: 7.4
+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 ed46b37ab8a28..a5146ea1e3e61 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 3913966cfdac0..adbc57b9f49c3 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 a51b0ed538864..3b438d8da5e0c 100644
--- a/drivers/leds/led-class.c
+++ b/drivers/leds/led-class.c
@@ -27,29 +27,19 @@ 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)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned int brightness;
- if (led_trigger_is_hw_controlled(led_cdev))
- return -ENODATA;
+ scoped_guard(mutex, &led_cdev->led_access) {
+ if (led_trigger_is_hw_controlled(led_cdev))
+ return -ENODATA;
- mutex_lock(&led_cdev->led_access);
- led_update_brightness(led_cdev);
- brightness = led_cdev->brightness;
- mutex_unlock(&led_cdev->led_access);
+ led_update_brightness(led_cdev);
+ brightness = led_cdev->brightness;
+ }
return sysfs_emit(buf, "%u\n", brightness);
}
@@ -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
@@ -596,6 +611,9 @@ int led_classdev_register_ext(struct device *parent,
led_trigger_set_default(led_cdev);
#endif
+ if (led_cdev->flags & LED_TRIG_HW_CHANGED)
+ led_trigger_init_hw_changed(led_cdev);
+
mutex_unlock(&led_cdev->led_access);
dev_dbg(parent, "Registered led device: %s\n",
@@ -616,6 +634,9 @@ void led_classdev_unregister(struct led_classdev *led_cdev)
if (IS_ERR_OR_NULL(led_cdev->dev))
return;
+ if (led_cdev->flags & LED_TRIG_HW_CHANGED)
+ led_trigger_destroy_hw_changed(led_cdev);
+
#ifdef CONFIG_LEDS_TRIGGERS
down_write(&led_cdev->trigger_lock);
if (led_cdev->trigger)
diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c
index b1223218bda11..a9d992a886161 100644
--- a/drivers/leds/led-triggers.c
+++ b/drivers/leds/led-triggers.c
@@ -7,9 +7,13 @@
* Author: Richard Purdie <rpurdie@openedhand.com>
*/
+#include <linux/bug.h>
+#include <linux/cleanup.h>
+#include <linux/compiler.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 +37,34 @@ 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);
+
+ /* Otherwise assume private triggers as always offloaded. */
+ 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 +195,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 +227,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 +305,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 +320,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 +374,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 +490,88 @@ 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);
+}
+
+static void led_trigger_hw_control_changed_worker(struct work_struct *work)
+{
+ struct led_classdev *led_cdev =
+ container_of(work, struct led_classdev, triggers_hw_changed_work);
+ bool activate = READ_ONCE(led_cdev->triggers_hw_changed);
+
+ scoped_guard(rwsem_read, &triggers_list_lock) {
+ struct led_trigger *trig;
+
+ 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);
+}
+
+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)
+{
+ /* Restricted to private triggers. */
+ if (WARN_ON(!(led_cdev->flags & LED_TRIG_HW_CHANGED) ||
+ !led_cdev->hw_control_trigger || !led_cdev->trigger_type))
+ return;
+
+ WRITE_ONCE(led_cdev->triggers_hw_changed, activate);
+
+ schedule_work(&led_cdev->triggers_hw_changed_work);
+}
+EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed);
+
+void led_trigger_init_hw_changed(struct led_classdev *led_cdev)
+{
+ INIT_WORK(&led_cdev->triggers_hw_changed_work, led_trigger_hw_control_changed_worker);
+}
+EXPORT_SYMBOL_GPL(led_trigger_init_hw_changed);
+
+void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev)
+{
+ disable_work_sync(&led_cdev->triggers_hw_changed_work);
+}
+EXPORT_SYMBOL_GPL(led_trigger_destroy_hw_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 1844d0cd5f528..6db83d0152775 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 ed6a47bbb44f9..32d40d176d3f1 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 bee46651e068f..bdac2336012e6 100644
--- a/drivers/leds/leds.h
+++ b/drivers/leds/leds.h
@@ -27,8 +27,18 @@ 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;
+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED
+void led_trigger_init_hw_changed(struct led_classdev *led_cdev);
+void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev);
+#else /* !CONFIG_LEDS_TRIGGERS_HW_CHANGED */
+static inline void led_trigger_init_hw_changed(struct led_classdev *led_cdev) { }
+static inline void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev) { }
+#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */
+
#endif /* __LEDS_H_INCLUDED */
diff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig
index c11282a74b5ac..a11d04ce4ab2c 100644
--- a/drivers/leds/trigger/Kconfig
+++ b/drivers/leds/trigger/Kconfig
@@ -9,6 +9,16 @@ menuconfig LEDS_TRIGGERS
if LEDS_TRIGGERS
+config LEDS_TRIGGERS_HW_CHANGED
+ bool "LED hardware-initiated trigger transition support"
+ default LEDS_BRIGHTNESS_HW_CHANGED
+ 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 5b4e92c14dbb4..2e9af48a74dbf 100644
--- a/drivers/leds/trigger/ledtrig-netdev.c
+++ b/drivers/leds/trigger/ledtrig-netdev.c
@@ -798,10 +798,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 5165949930739..a97852b0b2284 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 8213524504eeb..f86c11f51d0be 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -14,9 +14,12 @@
#include <linux/bitops.h>
#include <linux/bug.h>
#include <linux/cleanup.h>
+#include <linux/compiler.h>
+#include <linux/container_of.h>
#include <linux/debugfs.h>
#include <linux/delay.h>
#include <linux/device.h>
+#include <linux/dev_printk.h>
#include <linux/dmi.h>
#include <linux/i8042.h>
#include <linux/init.h>
@@ -26,9 +29,11 @@
#include <linux/kernel.h>
#include <linux/leds.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/platform_profile.h>
#include <linux/power_supply.h>
+#include <linux/printk.h>
#include <linux/rfkill.h>
#include <linux/seq_file.h>
#include <linux/string_choices.h>
@@ -134,10 +139,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 +171,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
@@ -200,10 +228,14 @@ struct ideapad_private {
bool ymc_ec_trigger : 1;
} features;
struct {
- bool initialized;
+ int initialized; /* 0: initializion pending/ongoing/failed
+ * >0: initializion finished
+ * <0: unregisteration ongoing/finished
+ */
int type;
struct led_classdev led;
- unsigned int last_brightness;
+ unsigned int last_hw_brightness;
+ struct mutex mutex; /* protects brightness tracking */
} kbd_bl;
struct {
bool initialized;
@@ -1592,7 +1624,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, unsigned 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 +1655,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 +1665,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,99 +1682,207 @@ 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,
+ unsigned 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;
+ guard(mutex)(&priv->kbd_bl.mutex);
- value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) |
+ if (ideapad_kbd_bl_check_tristate(type)) {
+ 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;
+ priv->kbd_bl.last_hw_brightness = hw_brightness;
return 0;
}
+static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned 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 unregistering: It must be the side effect of LED 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.
+ *
+ * It's needless to gate changes when initializing, as there may be a
+ * time margin between sysfs attribute creation and state change.
+ */
+ if (unlikely(priv->kbd_bl.initialized < 0))
+ 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);
+
+ guard(mutex)(&priv->kbd_bl.mutex);
+
+ return 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,
+ unsigned int hw_brightness)
+{
+ bool hw_control, last_hw_control;
+
+ if (!ideapad_kbd_bl_auto_trigger_registered || priv->kbd_bl.type != KBD_BL_TRISTATE_AUTO)
+ return;
+
+ hw_control = hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+ last_hw_control = priv->kbd_bl.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;
- if (!priv->kbd_bl.initialized)
+ if (unlikely(priv->kbd_bl.initialized <= 0))
return;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
+ guard(mutex)(&priv->kbd_bl.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. */
+
+ if (priv->kbd_bl.last_hw_brightness == hw_brightness)
return;
- priv->kbd_bl.last_brightness = brightness;
+ ideapad_kbd_bl_notify_hw_control(priv, hw_brightness);
+
+ priv->kbd_bl.last_hw_brightness = 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;
- if (WARN_ON(priv->kbd_bl.initialized))
+ if (WARN_ON(priv->kbd_bl.initialized > 0))
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;
+ priv->kbd_bl.initialized = 0;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
- return brightness;
+ err = devm_mutex_init(&priv->platform_device->dev, &priv->kbd_bl.mutex);
+ if (err)
+ return err;
+
+ hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+ if (hw_brightness < 0)
+ return hw_brightness;
+
+ 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;
+ 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,
+ "Cannot 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.trigger_type = &ideapad_kbd_bl_auto_trigger_type;
+ priv->kbd_bl.led.hw_control_trigger = ideapad_kbd_bl_auto_trigger.name;
+
+ /* 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();
+ }
+
+ /* Reject insane values. */
+ err = ideapad_kbd_bl_brightness_parse(priv, hw_brightness);
+ if (err < 0)
+ return err;
+
err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led);
if (err)
return err;
- priv->kbd_bl.initialized = true;
+ priv->kbd_bl.initialized = 1;
return 0;
}
static void ideapad_kbd_bl_exit(struct ideapad_private *priv)
{
- if (!priv->kbd_bl.initialized)
+ if (priv->kbd_bl.initialized <= 0)
return;
- priv->kbd_bl.initialized = false;
+ priv->kbd_bl.initialized = -1;
led_classdev_unregister(&priv->kbd_bl.led);
}
@@ -2548,17 +2701,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 +2732,9 @@ static void __exit ideapad_laptop_exit(void)
{
platform_driver_unregister(&ideapad_acpi_driver);
ideapad_wmi_driver_unregister();
+
+ 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 a515f075c29a1..93b3fd7e56364 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;
@@ -239,6 +240,11 @@ struct led_classdev {
struct kernfs_node *brightness_hw_changed_kn;
#endif
+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED
+ bool triggers_hw_changed;
+ struct work_struct triggers_hw_changed_work;
+#endif
+
/* Ensures consistent access to the LED class device */
struct mutex led_access;
};
@@ -485,6 +491,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 +540,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 +594,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 +615,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/09/02 07:22 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 15998 output: 25 thoughts: 750 Thoughts: **Analyzing the LED Patch Series** Okay, let's dive into this patch series. My initial impression is that it's touching quite a bit of the LED subsystem, so we need to be thorough. I see changes across a wide range of files: documentation, the core LED class driver, trigger implementations, and even some platform-specific drivers (Cros EC, Turris Omnia, and Lenovo laptops). That tells me this isn't just a minor tweak; this is likely adding significant new functionality or modifying existing behavior in a fairly pervasive way. First, I'm noting the files that are touched. It seems the core of the change lies in adding a `trigger_may_offload` sysfs attribute. That alone, affecting the sysfs interface, immediately grabs my attention. Anytime you alter the sysfs interface, you're opening the door to potential user-space interactions and, by extension, potential fuzzing targets. Changes to `brightness_show` and `brightness_store` in `led-class.c` are also pretty significant, affecting how brightness control works across all LEDs. Beyond that, the introduction of `led_trigger_is_hw_controlled`, `led_trigger_remove_hw_control`, and other functions, along with the `CONFIG_LEDS_TRIGGERS_HW_CHANGED` config, hints at hardware-initiated trigger transitions. The mention of `hw_triggered` implies some logic change within the existing trigger management. I immediately want to understand how this `hw_control_trigger` feature interacts with the existing trigger mechanisms and how the new sysfs attribute controls this interaction. The fact that the patch impacts drivers like the netdev trigger, and platform-specific drivers, suggests a wider reach. This isn't just about a new trigger type; it's about potentially integrating hardware control into existing LED subsystems. Now, I want to understand the reach of these changes. Since this is the LED subsystem, which is fundamental, I'm assuming that it is reasonably reachable in a variety of environments, including QEMU and virtualized scenarios. Any LED device, emulated or real, can interact with this. The fact that any emulated or real platform devices can expose these attributes in sysfs and trigger sysfs interactions adds potential attack surfaces. I need to quickly check the Kconfig dependencies to understand what configurations are required to enable this new functionality. Then I need to investigate the usage of these symbols with tools like Grep. I'll need to know what code paths will be hit and under what conditions. From there, I can determine if this is worth fuzzing, and, if so, what the best approach would be. This looks like it has fuzzing potential, since we have user space interaction and potential race conditions from hw control. |
| 4/2 | 2026/09/02 07:22 | 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"
} |
| 5/2 | 2026/09/02 07:22 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 17451 output: 35 thoughts: 24 Thoughts: Here's my summary, as if I were thinking these thoughts: **Analyzing the Kconfig for LED Triggers** Okay, time to dig into this. I need to understand how the LED triggers are configured, so I'm going to start with the `drivers/leds/trigger/Kconfig` file. I'm specifically interested in lines 1 through 30, as that's usually where the main options and dependencies are defined. I expect to find the overall structure of the trigger configuration there – likely the main menu entry and the core dependencies on other modules or kernel features. I'm anticipating that these lines will define the default behavior, whether certain triggers are enabled by default, and which platform configurations they rely upon. Getting the overview of this part is crucial for understanding the rest of the file and how this section ties into the kernel configuration. Let's see what we've got. |
| 6/2 | 2026/09/02 07:22 | tool | read-file |
0mArgs:{
"File": "drivers/leds/trigger/Kconfig",
"FirstLine": 1,
"LineCount": 30
}Results: {
"Contents": " 1:\t# SPDX-License-Identifier: GPL-2.0-only\n 2:\tmenuconfig LEDS_TRIGGERS\n 3:\t\tbool \"LED Trigger support\"\n 4:\t\tdepends on LEDS_CLASS\n 5:\t\thelp\n 6:\t\t This option enables trigger support for the leds class.\n 7:\t\t These triggers allow kernel events to drive the LEDs and can\n 8:\t\t be configured via sysfs. If unsure, say Y.\n 9:\t\n 10:\tif LEDS_TRIGGERS\n 11:\t\n 12:\tconfig LEDS_TRIGGERS_HW_CHANGED\n 13:\t\tbool \"LED hardware-initiated trigger transition support\"\n 14:\t\tdefault LEDS_BRIGHTNESS_HW_CHANGED\n 15:\t\thelp\n 16:\t\t This option enables support for hardware initiated hardware control\n 17:\t\t transitions, where the LED hardware autonomously switches between\n 18:\t\t \"none\" (i.e., no trigger) and its private trigger.\n 19:\t\n 20:\t\t See Documentation/leds/leds-class.rst for details.\n 21:\t\n 22:\tconfig LEDS_TRIGGER_TIMER\n 23:\t\ttristate \"LED Timer Trigger\"\n 24:\t\thelp\n 25:\t\t This allows LEDs to be controlled by a programmable timer\n 26:\t\t via sysfs. Some LED hardware can be programmed to start\n 27:\t\t blinking the LED without any further software interaction.\n 28:\t\t For more details read Documentation/leds/leds-class.rst.\n 29:\t\n 30:\t\t If unsure, say Y.\n"
} |
| 7/2 | 2026/09/02 07:22 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 18123 output: 30 thoughts: 46 Thoughts: Okay, here's my interpretation of those thoughts, tailored for an expert audience, presented in the first person: **Investigating LED Trigger Configuration** Alright, I need to get a handle on how these LED trigger options are being built and integrated. My immediate thought is to dig into the kernel's build configuration. I'm going to start by examining `drivers/leds/Kconfig`. That'll be the place to find the actual definitions of `LEDS_TRIGGERS` and `LEDS_TRIGGERS_HW_CHANGED`, or at least the configuration options that control them. I'm assuming that the Kconfig file will specify dependencies, default values, and a description. From there, I'll need to know where the code and drivers using these flags reside in the codebase. Then, I'll cross-reference that with `drivers/leds/Makefile`. The Makefile should reveal the build rules and dependencies, and more importantly, how these symbols influence the compilation process. This will probably include conditionals based on those Kconfig symbols, indicating which source files are compiled and linked depending on whether the options are enabled. Understanding the interaction between the Kconfig and Makefile will be key to understanding the full build configuration and how `LEDS_TRIGGERS` and `LEDS_TRIGGERS_HW_CHANGED` affect the driver build. This is how I'll determine the extent of their impact on the final kernel image. |
| 8/2 | 2026/09/02 07:23 | tool | grepper |
0mArgs:{
"Expression": "LEDS_TRIGGERS",
"PathPrefix": "drivers/leds"
}Results: {
"Output": "drivers/leds/Kconfig=202=config LEDS_CROS_EC\n--\ndrivers/leds/Kconfig-205-\tdepends on LEDS_CLASS_MULTICOLOR\ndrivers/leds/Kconfig:206:\tselect LEDS_TRIGGERS\ndrivers/leds/Kconfig-207-\tdefault MFD_CROS_EC_DEV\n--\ndrivers/leds/Kconfig=230=config LEDS_TURRIS_OMNIA\n--\ndrivers/leds/Kconfig-237-\tdepends on TURRIS_OMNIA_MCU_GPIO\ndrivers/leds/Kconfig:238:\tselect LEDS_TRIGGERS\ndrivers/leds/Kconfig-239-\thelp\n--\ndrivers/leds/Kconfig=342=config LEDS_COBALT_RAQ\n--\ndrivers/leds/Kconfig-344-\tdepends on LEDS_CLASS=y \u0026\u0026 (MIPS_COBALT || COMPILE_TEST)\ndrivers/leds/Kconfig:345:\tselect LEDS_TRIGGERS\ndrivers/leds/Kconfig-346-\thelp\n--\ndrivers/leds/Kconfig=358=config LEDS_SUNFIRE\n--\ndrivers/leds/Kconfig-361-\tdepends on SPARC64\ndrivers/leds/Kconfig:362:\tselect LEDS_TRIGGERS\ndrivers/leds/Kconfig-363-\thelp\n--\ndrivers/leds/Kconfig=993=config LEDS_ST1202\n--\ndrivers/leds/Kconfig-997-\tdepends on OF\ndrivers/leds/Kconfig:998:\tselect LEDS_TRIGGERS\ndrivers/leds/Kconfig-999-\tselect LEDS_TRIGGER_PATTERN\n--\ndrivers/leds/Makefile=7=obj-$(CONFIG_LEDS_CLASS_MULTICOLOR)\t+= led-class-multicolor.o\ndrivers/leds/Makefile:8:obj-$(CONFIG_LEDS_TRIGGERS)\t\t+= led-triggers.o\ndrivers/leds/Makefile-9-obj-$(CONFIG_LEDS_KUNIT_TEST)\t\t+= led-test.o\n--\ndrivers/leds/Makefile=120=obj-$(CONFIG_LEDS_CLASS_MULTICOLOR)\t+= rgb/\n--\ndrivers/leds/Makefile-122-# LED Triggers\ndrivers/leds/Makefile:123:obj-$(CONFIG_LEDS_TRIGGERS)\t\t+= trigger/\ndrivers/leds/Makefile-124-\n--\ndrivers/leds/led-class.c=91=static DEVICE_ATTR_RO(max_brightness);\ndrivers/leds/led-class.c-92-\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);\n--\ndrivers/leds/led-class.c=136=static const struct attribute_group *led_groups[] = {\ndrivers/leds/led-class.c-137-\t\u0026led_group,\ndrivers/leds/led-class.c:138:#ifdef CONFIG_LEDS_TRIGGERS\ndrivers/leds/led-class.c-139-\t\u0026led_trigger_group,\n--\ndrivers/leds/led-class.c=517=int led_classdev_register_ext(struct device *parent,\n--\ndrivers/leds/led-class.c-589-\tled_cdev-\u003ework_flags = 0;\ndrivers/leds/led-class.c:590:#ifdef CONFIG_LEDS_TRIGGERS\ndrivers/leds/led-class.c-591-\tinit_rwsem(\u0026led_cdev-\u003etrigger_lock);\n--\ndrivers/leds/led-class.c-609-\ndrivers/leds/led-class.c:610:#ifdef CONFIG_LEDS_TRIGGERS\ndrivers/leds/led-class.c-611-\tled_trigger_set_default(led_cdev);\n--\ndrivers/leds/led-class.c=632=void led_classdev_unregister(struct led_classdev *led_cdev)\n--\ndrivers/leds/led-class.c-639-\ndrivers/leds/led-class.c:640:#ifdef CONFIG_LEDS_TRIGGERS\ndrivers/leds/led-class.c-641-\tdown_write(\u0026led_cdev-\u003etrigger_lock);\n--\ndrivers/leds/led-triggers.c=491=EXPORT_SYMBOL_GPL(devm_led_trigger_register);\ndrivers/leds/led-triggers.c-492-\ndrivers/leds/led-triggers.c:493:#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\ndrivers/leds/led-triggers.c-494-\n--\ndrivers/leds/led-triggers.c=571=EXPORT_SYMBOL_GPL(led_trigger_destroy_hw_changed);\ndrivers/leds/led-triggers.c-572-\ndrivers/leds/led-triggers.c:573:#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */\ndrivers/leds/led-triggers.c-574-\n--\ndrivers/leds/leds.h=34=extern struct list_head leds_list;\ndrivers/leds/leds.h-35-\ndrivers/leds/leds.h:36:#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED\ndrivers/leds/leds.h-37-void led_trigger_init_hw_changed(struct led_classdev *led_cdev);\ndrivers/leds/leds.h=38=void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev);\ndrivers/leds/leds.h:39:#else /* !CONFIG_LEDS_TRIGGERS_HW_CHANGED */\ndrivers/leds/leds.h-40-static inline void led_trigger_init_hw_changed(struct led_classdev *led_cdev) { }\ndrivers/leds/leds.h=41=static inline void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev) { }\ndrivers/leds/leds.h:42:#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */\ndrivers/leds/leds.h-43-\n--\ndrivers/leds/trigger/Kconfig-1-# SPDX-License-Identifier: GPL-2.0-only\ndrivers/leds/trigger/Kconfig:2:menuconfig LEDS_TRIGGERS\ndrivers/leds/trigger/Kconfig-3-\tbool \"LED Trigger support\"\n--\ndrivers/leds/trigger/Kconfig-9-\ndrivers/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--\ndrivers/leds/trigger/Kconfig=109=comment \"iptables trigger is under Netfilter config (LED target)\"\ndrivers/leds/trigger/Kconfig:110:\tdepends on LEDS_TRIGGERS\ndrivers/leds/trigger/Kconfig-111-\n--\ndrivers/leds/trigger/Kconfig=158=config LEDS_TRIGGER_INPUT_EVENTS\n--\ndrivers/leds/trigger/Kconfig-173-\ndrivers/leds/trigger/Kconfig:174:endif # LEDS_TRIGGERS\n"
} |
| 9/2 | 2026/09/02 07:23 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 20448 output: 29 thoughts: 496 Thoughts: **Analyzing LED Driver Interactions and Fuzzing Potential** Okay, so I'm diving into this LED driver situation. My first thought is to zero in on `drivers/leds/led-class.c` and `drivers/leds/led-triggers.c`. I need to understand the exposed functions and interfaces involved, right? Gotta see what the landscape looks like. Then, I need to get a clear picture of what's been added in this patch. I've got a list from `led-triggers.c`: `led_trigger_is_hw_controlled`, `led_trigger_set`, `led_trigger_remove_hw_control`, `trigger_may_offload_show`, `led_trigger_notify_hw_control_changed`, `led_trigger_init_hw_changed`, and `led_trigger_destroy_hw_changed`. These are all `EXPORT_SYMBOL_GPL`, so definitely worth noting. From `led-class.c`, I see `brightness_show`, `brightness_store` and a `trigger_may_offload` sysfs attribute. Next, I'll need to look at `ideapad-laptop.c`. It's got some functions like `ideapad_kbd_bl_led_cdev_brightness_get`, `ideapad_kbd_bl_led_cdev_brightness_set`, `ideapad_kbd_bl_notify`, `ideapad_kbd_bl_auto_trigger_activate` and `ideapad_kbd_bl_auto_trigger_offloaded`. I'm curious what kind of interactions this has with the LED class. Also, let's not forget to check `netdev_trig_offloaded` in `ledtrig-netdev.c` . The big question now is whether this is something we can effectively fuzz. My understanding is syzkaller is a good candidate, but I need to confirm: can it access and manipulate the relevant LED class and trigger interfaces? I need to know if it can read and write to the `/sys/class/leds/...` sysfs files. If that checks out, I should see if there are standard or virtual LED devices/triggers, like `netdev` (`NETDEV_LEDS` / `LEDS_TRIGGER_NETDEV`). Finally, I'll need to figure out the relevant configuration options. Time to check around `LEDS_CLASS`, `LEDS_TRIGGERS`, `LEDS_TRIGGERS_HW_CHANGED`, `LEDS_TRIGGER_NETDEV`, and whatever else is relevant. This should paint a complete picture of the area. |
| 10/2 | 2026/09/02 07:23 | tool | grepper |
0mArgs:{
"Expression": "config LEDS_CLASS",
"PathPrefix": "drivers/leds"
}Results: {
"Output": "drivers/leds/Kconfig=21=if NEW_LEDS\ndrivers/leds/Kconfig-22-\ndrivers/leds/Kconfig:23:config LEDS_CLASS\ndrivers/leds/Kconfig-24-\ttristate \"LED Class Support\"\n--\ndrivers/leds/Kconfig-28-\ndrivers/leds/Kconfig:29:config LEDS_CLASS_FLASH\ndrivers/leds/Kconfig-30-\ttristate \"LED Flash Class Support\"\n--\ndrivers/leds/Kconfig-38-\ndrivers/leds/Kconfig:39:config LEDS_CLASS_MULTICOLOR\ndrivers/leds/Kconfig-40-\ttristate \"LED Multicolor Class Support\"\n"
} |
| 11/2 | 2026/09/02 07:23 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 21730 output: 31 thoughts: 17 Thoughts: Here's my attempt at summarizing the thought process in the first person, assuming the user is an expert: **Kconfig Dive: LEDS_TRIGGER_NETDEV** Okay, so I need to quickly zero in on `LEDS_TRIGGER_NETDEV`. I'm guessing there's something specific I need to know about its configuration. First things first, let's just make sure it's enabled and what, if any, dependencies are in play. I need to refresh my memory on the possible interactions this trigger might have with the network device subsystem. Are there any unusual interactions I need to be aware of? Are there specific edge cases the current code handles, or doesn't handle, that need to be revisited? Time to dig into Kconfig and see what's what. |
| 12/2 | 2026/09/02 07:23 | tool | grepper |
0mArgs:{
"Expression": "LEDS_TRIGGER_NETDEV",
"PathPrefix": "drivers/leds"
}Results: {
"Output": "drivers/leds/trigger/Kconfig=126=config LEDS_TRIGGER_PANIC\n--\ndrivers/leds/trigger/Kconfig-134-\ndrivers/leds/trigger/Kconfig:135:config LEDS_TRIGGER_NETDEV\ndrivers/leds/trigger/Kconfig-136-\ttristate \"LED Netdev Trigger\"\n--\ndrivers/leds/trigger/Makefile=14=obj-$(CONFIG_LEDS_TRIGGER_PANIC)\t+= ledtrig-panic.o\ndrivers/leds/trigger/Makefile:15:obj-$(CONFIG_LEDS_TRIGGER_NETDEV)\t+= ledtrig-netdev.o\ndrivers/leds/trigger/Makefile-16-obj-$(CONFIG_LEDS_TRIGGER_PATTERN)\t+= ledtrig-pattern.o\n"
} |
| 13/2 | 2026/09/02 07:23 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 22151 output: 33 thoughts: 21 Thoughts: Okay, here's my summary, taking the role of the expert: **Investigating LED Driver Dependencies** Alright, before I dive too deep, I need to get a handle on the `NEW_LEDS` dependency within the `drivers/leds/Kconfig` file. I need to understand what other components or settings it relies on. I always start with dependencies; it's the foundation of understanding how anything will fit into the whole. I want to make sure I'm not missing anything crucial. This is going to be key to ensure a smooth integration. |
| 14/2 | 2026/09/02 07:23 | tool | read-file |
0mArgs:{
"File": "drivers/leds/Kconfig",
"FirstLine": 1,
"LineCount": 40
}Results: {
"Contents": " 1:\t# SPDX-License-Identifier: GPL-2.0-only\n 2:\tconfig LEDS_GPIO_REGISTER\n 3:\t\tbool\n 4:\t\thelp\n 5:\t\t This option provides the function gpio_led_register_device.\n 6:\t\t As this function is used by arch code it must not be compiled as a\n 7:\t\t module.\n 8:\t\n 9:\t# This library does not depend on NEW_LEDS and must be independent so it can be\n 10:\t# selected from other subsystems (specifically backlight).\n 11:\tconfig LEDS_EXPRESSWIRE\n 12:\t\tbool\n 13:\t\tdepends on GPIOLIB\n 14:\t\n 15:\tmenuconfig NEW_LEDS\n 16:\t\tbool \"LED Support\"\n 17:\t\thelp\n 18:\t\t Say Y to enable Linux LED support. This allows control of supported\n 19:\t\t LEDs from both userspace and optionally, by kernel events (triggers).\n 20:\t\n 21:\tif NEW_LEDS\n 22:\t\n 23:\tconfig LEDS_CLASS\n 24:\t\ttristate \"LED Class Support\"\n 25:\t\thelp\n 26:\t\t This option enables the LED sysfs class in /sys/class/leds. You'll\n 27:\t\t need this to do anything useful with LEDs. If unsure, say Y.\n 28:\t\n 29:\tconfig LEDS_CLASS_FLASH\n 30:\t\ttristate \"LED Flash Class Support\"\n 31:\t\tdepends on LEDS_CLASS\n 32:\t\thelp\n 33:\t\t This option enables the flash LED sysfs class in /sys/class/leds.\n 34:\t\t It wraps LED Class and adds flash LEDs specific sysfs attributes\n 35:\t\t and kernel internal API to it. You'll need this to provide support\n 36:\t\t for the flash related features of a LED device. It can be built\n 37:\t\t as a module.\n 38:\t\n 39:\tconfig LEDS_CLASS_MULTICOLOR\n 40:\t\ttristate \"LED Multicolor Class Support\"\n"
} |
| 15/2 | 2026/09/02 07:23 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 22877 output: 34 thoughts: 16 Thoughts: Here's a summary of my thought process, as if I were thinking it: **Checking `LEDS_BRIGHTNESS_HW_CHANGED`** Okay, time to dive into this. Let's start with `LEDS_BRIGHTNESS_HW_CHANGED`. That flag immediately tells me something significant has changed in the hardware brightness level. This means a direct, physical alteration occurred, bypassing the typical software control. This is important to understand because a hardware change dictates how the software responds and updates. I need to know where this flag is set, what triggers it, and the immediate consequences when it's asserted. Does this lead to an interrupt handler, a polling loop, or perhaps a direct write to a register? And what specific registers are involved? Are we talking about a simple analog adjustment or a more complex PWM implementation? Ultimately, I must understand how the system reconciles the new hardware value with the intended software state. Are there any synchronization issues to worry about? I need to analyze the code that reads and reacts to this flag to ensure consistency and prevent conflicting behaviors. Let's see...where's the definition, and let's trace it through the system. |
| 16/2 | 2026/09/02 07:23 | tool | grepper |
0mArgs:{
"Expression": "LEDS_BRIGHTNESS_HW_CHANGED",
"PathPrefix": "drivers/leds"
}Results: {
"Output": "drivers/leds/Kconfig=39=config LEDS_CLASS_MULTICOLOR\n--\ndrivers/leds/Kconfig-48-\ndrivers/leds/Kconfig:49:config LEDS_BRIGHTNESS_HW_CHANGED\ndrivers/leds/Kconfig-50-\tbool \"LED Class brightness_hw_changed attribute support\"\n--\ndrivers/leds/led-class.c=136=static const struct attribute_group *led_groups[] = {\n--\ndrivers/leds/led-class.c-143-\ndrivers/leds/led-class.c:144:#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED\ndrivers/leds/led-class.c-145-static ssize_t brightness_hw_changed_show(struct device *dev,\n--\ndrivers/leds/led-class.c=517=int led_classdev_register_ext(struct device *parent,\n--\ndrivers/leds/led-class.c-592-#endif\ndrivers/leds/led-class.c:593:#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED\ndrivers/leds/led-class.c-594-\tled_cdev-\u003ebrightness_hw_changed = -1;\n--\ndrivers/leds/trigger/Kconfig=12=config LEDS_TRIGGERS_HW_CHANGED\ndrivers/leds/trigger/Kconfig-13-\tbool \"LED hardware-initiated trigger transition support\"\ndrivers/leds/trigger/Kconfig:14:\tdefault LEDS_BRIGHTNESS_HW_CHANGED\ndrivers/leds/trigger/Kconfig-15-\thelp\n"
} |
| 17/2 | 2026/09/02 07:23 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 23585 output: 39 thoughts: 28 Thoughts: Here's a summary of my thought process: **Kconfig Inspection for IDEAPAD_LAPTOP** Okay, time to dig into the kernel configuration. I need to see what's happening with that `IDEAPAD_LAPTOP` symbol, specifically where and how it's defined and used. My instinct is that it will be in the `drivers/platform/x86/lenovo/Kconfig` file. That's usually the go-to spot for Lenovo-specific platform drivers and features, and the `Kconfig` files are where the configuration options are declared. I know these paths like the back of my hand, but I still do a quick mental check – yep, that's the likely location. Let's get to it. Time to open that `Kconfig` and see what it reveals. |
| 18/2 | 2026/09/02 07:23 | tool | read-file |
0mArgs:{
"File": "drivers/platform/x86/lenovo/Kconfig",
"FirstLine": 1,
"LineCount": 40
}Results: {
"Contents": " 1:\t# SPDX-License-Identifier: GPL-2.0-only\n 2:\t#\n 3:\t# Lenovo X86 Platform Specific Drivers\n 4:\t#\n 5:\t\n 6:\tconfig IDEAPAD_LAPTOP\n 7:\t\ttristate \"Lenovo IdeaPad Laptop Extras\"\n 8:\t\tdepends on ACPI\n 9:\t\tdepends on ACPI_BATTERY\n 10:\t\tdepends on RFKILL \u0026\u0026 INPUT\n 11:\t\tdepends on SERIO_I8042\n 12:\t\tdepends on BACKLIGHT_CLASS_DEVICE\n 13:\t\tdepends on ACPI_VIDEO || ACPI_VIDEO = n\n 14:\t\tdepends on ACPI_WMI || ACPI_WMI = n\n 15:\t\tselect ACPI_PLATFORM_PROFILE\n 16:\t\tselect INPUT_SPARSEKMAP\n 17:\t\tselect NEW_LEDS\n 18:\t\tselect LEDS_CLASS\n 19:\t\tselect LEDS_TRIGGERS\n 20:\t\thelp\n 21:\t\t This is a driver for Lenovo IdeaPad netbooks contains drivers for\n 22:\t\t rfkill switch, hotkey, fan control and backlight control.\n 23:\t\n 24:\tconfig LENOVO_WMI_HOTKEY_UTILITIES\n 25:\t\ttristate \"Lenovo Hotkey Utility WMI extras driver\"\n 26:\t\tdepends on ACPI_WMI\n 27:\t\tselect NEW_LEDS\n 28:\t\tselect LEDS_CLASS\n 29:\t\timply IDEAPAD_LAPTOP\n 30:\t\thelp\n 31:\t\t This driver provides WMI support for Lenovo customized hotkeys function,\n 32:\t\t such as LED control for audio/mic mute event for Ideapad, YOGA, XiaoXin,\n 33:\t\t Gaming, ThinkBook and so on.\n 34:\t\n 35:\tconfig LENOVO_WMI_CAMERA\n 36:\t\ttristate \"Lenovo WMI Camera Button driver\"\n 37:\t\tdepends on ACPI_WMI\n 38:\t\tdepends on INPUT\n 39:\t\thelp\n 40:\t\t This driver provides support for Lenovo camera button. The Camera\n"
} |
| 19/2 | 2026/09/02 07:23 | llm | patch-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 24379 output: 241 thoughts: 1232 Thoughts: **Fuzzing Feasibility Analysis: LED Subsystem Patch** Okay, let's break down whether this LED subsystem patch is a good target for fuzzing. First, I need to understand the core changes. It looks like we're dealing with extensive modifications to the LED core, including updates to the sysfs interface (specifically the introduction of `trigger_may_offload`), improved locking in brightness handling, and, critically, a new framework for hardware-initiated trigger transitions (`CONFIG_LEDS_TRIGGERS_HW_CHANGED`). A new private trigger `ideapad-auto` has also been added in `ideapad-laptop.c`, and the netdev trigger has been modified too. The critical thing is reachability, and yes, it seems highly reachable. The sysfs interface for LEDs is a standard avenue from userspace. Any LED device (keyboard, network, virtual, etc.) exposes the relevant attributes (`trigger`, `brightness`, `trigger_may_offload`). Interacting with those sysfs files should exercise the core logic, hitting functions like `trigger_may_offload_show`, `brightness_show`, `brightness_store`, `led_trigger_set`, etc. Now, let's identify the key entry points, the functions that likely constitute our "FocusSymbols". Based on the code, they seem to be: `trigger_may_offload_show`, `led_trigger_is_hw_controlled`, `led_trigger_remove_hw_control`, `led_trigger_set`, and `led_trigger_notify_hw_control_changed`. We should double-check these in `led-triggers.c` to be certain. We'll need to note the configuration options. It looks like we'll need `NEW_LEDS`, `LEDS_CLASS`, `LEDS_TRIGGERS`, `LEDS_TRIGGERS_HW_CHANGED`, `LEDS_TRIGGER_NETDEV`, `LEDS_BRIGHTNESS_HW_CHANGED`, and `IDEAPAD_LAPTOP`. I need to verify that these names are properly spelled. Finally, a quick review of the modified files is necessary: `drivers/leds/led-class.c`, `drivers/leds/led-triggers.c`, `drivers/leds/trigger/ledtrig-netdev.c`, `drivers/platform/x86/lenovo/ideapad-laptop.c`, `drivers/leds/leds-cros_ec.c`, `drivers/leds/leds-turris-omnia.c`. Based on the nature of the changes, the answer is Yes! This is definitely worth fuzzing. We're dealing with functional changes in core driver code, particularly around trigger management, locking, concurrency between user space and kernel workqueues, and the new hardware-triggered transitions. This has a high potential for exploitable bugs. The code is reachable, and the potential impact is significant. So, I will confirm: * **FocusSymbols:** `trigger_may_offload_show`, `led_trigger_is_hw_controlled`, `led_trigger_remove_hw_control`, `led_trigger_set`, `led_trigger_notify_hw_control_changed` * **EnableConfigs:** `NEW_LEDS`, `LEDS_CLASS`, `LEDS_TRIGGERS`, `LEDS_TRIGGERS_HW_CHANGED`, `LEDS_TRIGGER_NETDEV`, `LEDS_BRIGHTNESS_HW_CHANGED`, `IDEAPAD_LAPTOP`. Now, time to prepare the call to `set-results`. |
| 20/2 | 2026/09/02 07:24 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"NEW_LEDS",
"LEDS_CLASS",
"LEDS_TRIGGERS",
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"LEDS_BRIGHTNESS_HW_CHANGED",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"trigger_may_offload_show",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"led_trigger_set",
"led_trigger_notify_hw_control_changed"
],
"Reasoning": "The patch series introduces a generic sysfs attribute (trigger_may_offload) for queryable hardware-offloaded LED triggers, updates core brightness get/set locking and trigger removal logic, and implements support for hardware-initiated trigger transitions (CONFIG_LEDS_TRIGGERS_HW_CHANGED) along with updates to LED trigger drivers. These changes modify reachable LED class/trigger sysfs interfaces and core trigger lifecycle handling.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"NEW_LEDS",
"LEDS_CLASS",
"LEDS_TRIGGERS",
"LEDS_TRIGGERS_HW_CHANGED",
"LEDS_TRIGGER_NETDEV",
"LEDS_BRIGHTNESS_HW_CHANGED",
"IDEAPAD_LAPTOP"
],
"FocusSymbols": [
"trigger_may_offload_show",
"led_trigger_is_hw_controlled",
"led_trigger_remove_hw_control",
"led_trigger_set",
"led_trigger_notify_hw_control_changed"
],
"Reasoning": "The patch series introduces a generic sysfs attribute (trigger_may_offload) for queryable hardware-offloaded LED triggers, updates core brightness get/set locking and trigger removal logic, and implements support for hardware-initiated trigger transitions (CONFIG_LEDS_TRIGGERS_HW_CHANGED) along with updates to LED trigger drivers. These changes modify reachable LED class/trigger sysfs interfaces and core trigger lifecycle handling.",
"WorthFuzzing": true
} |
| 21/1 | 2026/09/02 07:24 | action | If |
0mArgs:{
"WorthFuzzing": true
}Results: null |
| 22/2 | 2026/09/02 07:24 | agent | kmsan-evaluator |
0mModel:core Args: null Results: {
"KMSANReasoning": "The patch adds hardware-controlled LED trigger offload infrastructure, the `trigger_may_offload` sysfs attribute, hardware trigger transition notifications via a workqueue, and updates drivers including ideapad-laptop and leds-cros_ec.\n\n1. No data structures are copied to userspace via copy_to_user, netlink, ioctl, or socket buffers with uninitialized padding or fields. Userspace exposure is limited to standard sysfs_emit string formatting.\n2. All local variables and struct fields (e.g., triggers_hw_changed, last_hw_brightness, hw_brightness) are explicitly initialized before use or branching.\n3. Concurrency and lifecycle handling (such as workqueue flush/destruction and mutex/rwsem locks) fall under the purview of LOCKDEP and standard KASAN rather than KMSAN.\n\nThere are no uninitialized memory access or info-leak risks that would require dedicated KMSAN fuzzing.",
"NeedsKMSAN": false
}Instruction: You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.
CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
a comprehensive suite of debugging tools and sanitizers, including KASAN
(out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
(locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
or page allocations) and kernel-to-user memory info-leaks.
Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.
Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.
Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
attributes, ioctl output arguments, socket options, or BPF buffers) where fields
or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
uninitialized bytes of existing buffers.
Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
dereferences, locking deadlocks, or use-after-free bugs (these are already caught
by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.
Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
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 d07a5e3c92114c75453172a9452ab7a25bc34b52
Author: syz-cluster <triage@syzkaller.com>
Date: Wed Sep 2 07:22:27 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 d4c918cc11a12..94c331d9c4105 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: September 2026
+KernelVersion: 7.4
+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 ed46b37ab8a28..a5146ea1e3e61 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 3913966cfdac0..adbc57b9f49c3 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 a51b0ed538864..3b438d8da5e0c 100644
--- a/drivers/leds/led-class.c
+++ b/drivers/leds/led-class.c
@@ -27,29 +27,19 @@ 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)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned int brightness;
- if (led_trigger_is_hw_controlled(led_cdev))
- return -ENODATA;
+ scoped_guard(mutex, &led_cdev->led_access) {
+ if (led_trigger_is_hw_controlled(led_cdev))
+ return -ENODATA;
- mutex_lock(&led_cdev->led_access);
- led_update_brightness(led_cdev);
- brightness = led_cdev->brightness;
- mutex_unlock(&led_cdev->led_access);
+ led_update_brightness(led_cdev);
+ brightness = led_cdev->brightness;
+ }
return sysfs_emit(buf, "%u\n", brightness);
}
@@ -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
@@ -596,6 +611,9 @@ int led_classdev_register_ext(struct device *parent,
led_trigger_set_default(led_cdev);
#endif
+ if (led_cdev->flags & LED_TRIG_HW_CHANGED)
+ led_trigger_init_hw_changed(led_cdev);
+
mutex_unlock(&led_cdev->led_access);
dev_dbg(parent, "Registered led device: %s\n",
@@ -616,6 +634,9 @@ void led_classdev_unregister(struct led_classdev *led_cdev)
if (IS_ERR_OR_NULL(led_cdev->dev))
return;
+ if (led_cdev->flags & LED_TRIG_HW_CHANGED)
+ led_trigger_destroy_hw_changed(led_cdev);
+
#ifdef CONFIG_LEDS_TRIGGERS
down_write(&led_cdev->trigger_lock);
if (led_cdev->trigger)
diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c
index b1223218bda11..a9d992a886161 100644
--- a/drivers/leds/led-triggers.c
+++ b/drivers/leds/led-triggers.c
@@ -7,9 +7,13 @@
* Author: Richard Purdie <rpurdie@openedhand.com>
*/
+#include <linux/bug.h>
+#include <linux/cleanup.h>
+#include <linux/compiler.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 +37,34 @@ 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);
+
+ /* Otherwise assume private triggers as always offloaded. */
+ 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 +195,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 +227,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 +305,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 +320,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 +374,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 +490,88 @@ 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);
+}
+
+static void led_trigger_hw_control_changed_worker(struct work_struct *work)
+{
+ struct led_classdev *led_cdev =
+ container_of(work, struct led_classdev, triggers_hw_changed_work);
+ bool activate = READ_ONCE(led_cdev->triggers_hw_changed);
+
+ scoped_guard(rwsem_read, &triggers_list_lock) {
+ struct led_trigger *trig;
+
+ 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);
+}
+
+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)
+{
+ /* Restricted to private triggers. */
+ if (WARN_ON(!(led_cdev->flags & LED_TRIG_HW_CHANGED) ||
+ !led_cdev->hw_control_trigger || !led_cdev->trigger_type))
+ return;
+
+ WRITE_ONCE(led_cdev->triggers_hw_changed, activate);
+
+ schedule_work(&led_cdev->triggers_hw_changed_work);
+}
+EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed);
+
+void led_trigger_init_hw_changed(struct led_classdev *led_cdev)
+{
+ INIT_WORK(&led_cdev->triggers_hw_changed_work, led_trigger_hw_control_changed_worker);
+}
+EXPORT_SYMBOL_GPL(led_trigger_init_hw_changed);
+
+void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev)
+{
+ disable_work_sync(&led_cdev->triggers_hw_changed_work);
+}
+EXPORT_SYMBOL_GPL(led_trigger_destroy_hw_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 1844d0cd5f528..6db83d0152775 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 ed6a47bbb44f9..32d40d176d3f1 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 bee46651e068f..bdac2336012e6 100644
--- a/drivers/leds/leds.h
+++ b/drivers/leds/leds.h
@@ -27,8 +27,18 @@ 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;
+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED
+void led_trigger_init_hw_changed(struct led_classdev *led_cdev);
+void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev);
+#else /* !CONFIG_LEDS_TRIGGERS_HW_CHANGED */
+static inline void led_trigger_init_hw_changed(struct led_classdev *led_cdev) { }
+static inline void led_trigger_destroy_hw_changed(struct led_classdev *led_cdev) { }
+#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */
+
#endif /* __LEDS_H_INCLUDED */
diff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig
index c11282a74b5ac..a11d04ce4ab2c 100644
--- a/drivers/leds/trigger/Kconfig
+++ b/drivers/leds/trigger/Kconfig
@@ -9,6 +9,16 @@ menuconfig LEDS_TRIGGERS
if LEDS_TRIGGERS
+config LEDS_TRIGGERS_HW_CHANGED
+ bool "LED hardware-initiated trigger transition support"
+ default LEDS_BRIGHTNESS_HW_CHANGED
+ 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 5b4e92c14dbb4..2e9af48a74dbf 100644
--- a/drivers/leds/trigger/ledtrig-netdev.c
+++ b/drivers/leds/trigger/ledtrig-netdev.c
@@ -798,10 +798,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 5165949930739..a97852b0b2284 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 8213524504eeb..f86c11f51d0be 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -14,9 +14,12 @@
#include <linux/bitops.h>
#include <linux/bug.h>
#include <linux/cleanup.h>
+#include <linux/compiler.h>
+#include <linux/container_of.h>
#include <linux/debugfs.h>
#include <linux/delay.h>
#include <linux/device.h>
+#include <linux/dev_printk.h>
#include <linux/dmi.h>
#include <linux/i8042.h>
#include <linux/init.h>
@@ -26,9 +29,11 @@
#include <linux/kernel.h>
#include <linux/leds.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/platform_profile.h>
#include <linux/power_supply.h>
+#include <linux/printk.h>
#include <linux/rfkill.h>
#include <linux/seq_file.h>
#include <linux/string_choices.h>
@@ -134,10 +139,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 +171,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
@@ -200,10 +228,14 @@ struct ideapad_private {
bool ymc_ec_trigger : 1;
} features;
struct {
- bool initialized;
+ int initialized; /* 0: initializion pending/ongoing/failed
+ * >0: initializion finished
+ * <0: unregisteration ongoing/finished
+ */
int type;
struct led_classdev led;
- unsigned int last_brightness;
+ unsigned int last_hw_brightness;
+ struct mutex mutex; /* protects brightness tracking */
} kbd_bl;
struct {
bool initialized;
@@ -1592,7 +1624,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, unsigned 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 +1655,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 +1665,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,99 +1682,207 @@ 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,
+ unsigned 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;
+ guard(mutex)(&priv->kbd_bl.mutex);
- value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) |
+ if (ideapad_kbd_bl_check_tristate(type)) {
+ 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;
+ priv->kbd_bl.last_hw_brightness = hw_brightness;
return 0;
}
+static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned 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 unregistering: It must be the side effect of LED 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.
+ *
+ * It's needless to gate changes when initializing, as there may be a
+ * time margin between sysfs attribute creation and state change.
+ */
+ if (unlikely(priv->kbd_bl.initialized < 0))
+ 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);
+
+ guard(mutex)(&priv->kbd_bl.mutex);
+
+ return 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,
+ unsigned int hw_brightness)
+{
+ bool hw_control, last_hw_control;
+
+ if (!ideapad_kbd_bl_auto_trigger_registered || priv->kbd_bl.type != KBD_BL_TRISTATE_AUTO)
+ return;
+
+ hw_control = hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+ last_hw_control = priv->kbd_bl.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;
- if (!priv->kbd_bl.initialized)
+ if (unlikely(priv->kbd_bl.initialized <= 0))
return;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
+ guard(mutex)(&priv->kbd_bl.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. */
+
+ if (priv->kbd_bl.last_hw_brightness == hw_brightness)
return;
- priv->kbd_bl.last_brightness = brightness;
+ ideapad_kbd_bl_notify_hw_control(priv, hw_brightness);
+
+ priv->kbd_bl.last_hw_brightness = 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;
- if (WARN_ON(priv->kbd_bl.initialized))
+ if (WARN_ON(priv->kbd_bl.initialized > 0))
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;
+ priv->kbd_bl.initialized = 0;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
- return brightness;
+ err = devm_mutex_init(&priv->platform_device->dev, &priv->kbd_bl.mutex);
+ if (err)
+ return err;
+
+ hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+ if (hw_brightness < 0)
+ return hw_brightness;
+
+ 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;
+ 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,
+ "Cannot 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.trigger_type = &ideapad_kbd_bl_auto_trigger_type;
+ priv->kbd_bl.led.hw_control_trigger = ideapad_kbd_bl_auto_trigger.name;
+
+ /* 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();
+ }
+
+ /* Reject insane values. */
+ err = ideapad_kbd_bl_brightness_parse(priv, hw_brightness);
+ if (err < 0)
+ return err;
+
err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led);
if (err)
return err;
- priv->kbd_bl.initialized = true;
+ priv->kbd_bl.initialized = 1;
return 0;
}
static void ideapad_kbd_bl_exit(struct ideapad_private *priv)
{
- if (!priv->kbd_bl.initialized)
+ if (priv->kbd_bl.initialized <= 0)
return;
- priv->kbd_bl.initialized = false;
+ priv->kbd_bl.initialized = -1;
led_classdev_unregister(&priv->kbd_bl.led);
}
@@ -2548,17 +2701,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 +2732,9 @@ static void __exit ideapad_laptop_exit(void)
{
platform_driver_unregister(&ideapad_acpi_driver);
ideapad_wmi_driver_unregister();
+
+ 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 a515f075c29a1..93b3fd7e56364 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;
@@ -239,6 +240,11 @@ struct led_classdev {
struct kernfs_node *brightness_hw_changed_kn;
#endif
+#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED
+ bool triggers_hw_changed;
+ struct work_struct triggers_hw_changed_work;
+#endif
+
/* Ensures consistent access to the LED class device */
struct mutex led_access;
};
@@ -485,6 +491,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 +540,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 +594,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 +615,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,
|
| 23/3 | 2026/09/02 07:24 | llm | kmsan-evaluator |
0mModel:gemini-3.7-flash Args: null Results: null Tokens: input: 15995 output: 225 thoughts: 2030 Thoughts: **Patch Analysis and KMSAN Assessment** Okay, let's break down this LED trigger patch. My initial focus is always on potential security vulnerabilities, especially uninitialized memory reads or information leaks. This patch is pretty focused. It's adding a new `trigger_may_offload` sysfs attribute to the LED class, a corresponding `offloaded` callback in the `led_trigger` structure, and implementing a mechanism for hardware-initiated trigger transitions. Critically, it then integrates this with several specific drivers: `leds-cros_ec`, `leds-turris-omnia`, `ledtrig-netdev`, and the `ideapad-laptop`. That last one catches my eye – ACPI and keyboard backlights can be touchy from a security perspective. So, first, I need to methodically check for uninitialized memory usage. I'm going through the code. I immediately look at that `trigger_may_offload_show` function. The `sysfs_emit` seems safe here. I am verifying that `hw_control_trigger` is guaranteed to be non-NULL when read. I see that the code checks for this condition in `led_trigger_is_visible`, so the sysfs entry will not be created. `offloaded` and `hit` are straightforward booleans. It's all looking good so far. Now I delve into `__led_trigger_is_hw_controlled`. This one is key, as it determines if a trigger is considered "offloaded." The code does a decent job checking for NULL triggers and ensures the trigger name matches. It also has a fall-back if the trigger does not implement `offloaded`, so it is looking like the code is handling all possible return paths. Then I check the workqueue in `led-triggers.c`. `led_trigger_hw_control_changed_worker` reads `ledcdev->triggers_hw_changed`. I'm checking the initialization. The variable is written using `WRITE_ONCE` in `led_trigger_notify_hw_control_changed` and is initialized in `led_classdev_register_ext`. This looks sound; the `READ_ONCE`/`WRITE_ONCE` pair is correct for workqueue interaction. Next, I review the `ideapad-laptop.c` code; I am making sure the values are initialized from ACPI calls. After quickly scanning, I find that ACPI method calls are checked, and there are no uninitialized variables being read. Okay, let's step back and consider potential information leaks, I'm checking for any `copy_to_user`, netlink messages, socket operations, struct padding issues, or reads from uninitialized stack or heap memory. This patch focuses on sysfs outputs and internal callbacks. There are no changes related to networking, BPF, or direct user-space memory copies. The only interaction with userspace is via `sysfs_emit`, which constructs strings. The data types are basic ints, booleans, and initialized structs. I see no direct threats. Now I am going through the criteria for KMSAN. The critical questions are: does the patch introduce any new kernel structures sent to userspace with uninitialized fields? Are there any branching or conditional logic that depends on potentially uninitialized data? Are complex data structures allocated with uninitialized fields? Does the code access uninitialized bytes of existing buffers? Based on the analysis, I am concluding that all the variables are initialized before use, and there are no info-leaks. Therefore, `NeedsKMSAN = false`. This patch doesn't introduce any new risks from uninitialized memory usage, and thus I am confident in this conclusion. Let's call `set-results`. |
| 24/3 | 2026/09/02 07:25 | tool | set-results |
0mArgs:{
"KMSANReasoning": "The patch adds hardware-controlled LED trigger offload infrastructure, the `trigger_may_offload` sysfs attribute, hardware trigger transition notifications via a workqueue, and updates drivers including ideapad-laptop and leds-cros_ec.\n\n1. No data structures are copied to userspace via copy_to_user, netlink, ioctl, or socket buffers with uninitialized padding or fields. Userspace exposure is limited to standard sysfs_emit string formatting.\n2. All local variables and struct fields (e.g., triggers_hw_changed, last_hw_brightness, hw_brightness) are explicitly initialized before use or branching.\n3. Concurrency and lifecycle handling (such as workqueue flush/destruction and mutex/rwsem locks) fall under the purview of LOCKDEP and standard KASAN rather than KMSAN.\n\nThere are no uninitialized memory access or info-leak risks that would require dedicated KMSAN fuzzing.",
"NeedsKMSAN": false
}Results: {
"KMSANReasoning": "The patch adds hardware-controlled LED trigger offload infrastructure, the `trigger_may_offload` sysfs attribute, hardware trigger transition notifications via a workqueue, and updates drivers including ideapad-laptop and leds-cros_ec.\n\n1. No data structures are copied to userspace via copy_to_user, netlink, ioctl, or socket buffers with uninitialized padding or fields. Userspace exposure is limited to standard sysfs_emit string formatting.\n2. All local variables and struct fields (e.g., triggers_hw_changed, last_hw_brightness, hw_brightness) are explicitly initialized before use or branching.\n3. Concurrency and lifecycle handling (such as workqueue flush/destruction and mutex/rwsem locks) fall under the purview of LOCKDEP and standard KASAN rather than KMSAN.\n\nThere are no uninitialized memory access or info-leak risks that would require dedicated KMSAN fuzzing.",
"NeedsKMSAN": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|