Linux LED subsystem development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Aaron Erhardt" <aer@tuxedocomputers.com>
Cc: linux-input@vger.kernel.org, linux-leds@vger.kernel.org,
	dmitry.torokhov@gmail.com, lee@kernel.org
Subject: Re: [PATCH v7 1/3] HID: lamparray: add new LampArray helper module
Date: Tue, 20 Oct 2026 10:21:08 +0000	[thread overview]
Message-ID: <sashiko-outbox-178826@kernel.org> (raw)
In-Reply-To: <20261020-hid-lamparray-v7-1-47418b667b47@tuxedocomputers.com>

Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] HID: lamparray: broken feature report parsing with hid_report_raw_event()
- [High] HID: lamparray: missing LampUpdateComplete flag in lamparray_hw_set_state()
- [Medium] HID: lamparray: LED_UNREGISTERING flag left set in lamparray_register_led()
- [Medium] HID: lamparray: brightness and intensity races in LED callbacks and restore
- [Medium] HID: lamparray: premature sysfs/LED exposure in lamparray_register()
- [Medium] HID: lamparray: inverted teardown ordering in lamparray_unregister()
- [Medium] HID: lamparray: mixing __free() and guard() cleanup with goto labels
- [Medium] HID: lamparray: missing LampId request in lamparray_get_lamp_attributes()
- [Low] HID: lamparray: stale miscdevice references in hid-lamparray.h kernel-doc
--

commit 5808d3d964362ce7a1a1b72a82eceb2d7c489b7a
Author: Aaron Erhardt <aer@tuxedocomputers.com>

HID: lamparray: add new LampArray helper module

This commit adds a hid-lamparray helper module that provides basic single-zone
RGB LED support for HID devices exposing a Lighting/LampArray application
collection (usage page 0x59).

> diff --git a/drivers/hid/hid-lamparray.c b/drivers/hid/hid-lamparray.c
> new file mode 100644
> index 0000000000000..fb3082f6d19a0
> --- /dev/null
> +++ b/drivers/hid/hid-lamparray.c
> @@ -0,0 +1,858 @@

[ ... ]

> +static int lamparray_read_attributes_report(struct lamparray_device *ldev)
> +{
> +	struct hid_device *hdev = ldev->hdev;
> +	struct hid_report *report;
> +	int ret;
> +	size_t len;
> +
> +	if (!ldev->lamp_count.field) {
> +		hid_warn(hdev, "No LampCount field found\n");
> +		return -ENODEV;
> +	}
> +
> +	if (!ldev->lamparray_kind.field) {
> +		hid_warn(hdev, "No LampArrayKind field found\n");
> +		return -ENODEV;
> +	}
> +
> +	report = ldev->lamp_count.field->report;
> +
> +	if (!report) {
> +		hid_warn(hdev, "LampCount field has no report\n");
> +		return -ENODEV;
> +	}
> +
> +	len = hid_report_len(report);
> +	u8 *buf __free(kfree) = kmalloc(len, GFP_KERNEL);
> +	if (!buf)
> +		return -ENOMEM;
> +
> +	guard(mutex)(&ldev->dev_lock);
> +
> +	/* Update values */
> +	ret = hid_hw_raw_request(hdev, report->id, buf, len,
> +				 HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +	if (ret < 0) {
> +		hid_warn(hdev, "Failed to request LampArrayAttributesReport: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = hid_report_raw_event(hdev, HID_FEATURE_REPORT, buf, len, ret, 0);

[Severity: High]
Can this sequence in lamparray_read_attributes_report() and
lamparray_get_lamp_attributes() misparse feature reports or leak uninitialized
heap bytes?

First, buf is allocated with kmalloc(len, GFP_KERNEL) without zeroing it or
setting buf[0] = report->id, and buf[0] is not validated after
hid_hw_raw_request(). If a low-level transport driver does not write buf[0] on
HID_REQ_GET_REPORT (for example, handle_lamp_array_attributes_report() and
handle_lamp_attributes_response_report() in
drivers/platform/x86/tuxedo/nb04/wmi_ab.c where report_id is const u8, or
ishtp_raw_request() on timeout), buf[0] retains uninitialized heap memory.
When hid_report_raw_event() runs:

drivers/hid/hid-core.c:hid_report_raw_event() {
	report = hid_get_report(report_enum, data);
	...
	if (hid->claimed & HID_CLAIMED_HIDRAW) {
		ret = hidraw_report_event(hid, data, size);
		if (ret)
			return ret;
	}
}

hid_get_report() reads the uninitialized buf[0] to index
report_enum->report_id_hash[n] (failing to match the report or updating a
different report), and hidraw_report_event() copies the uninitialized buffer
and synchronous GET_REPORT reply to userspace hidraw clients.

Second, if the feature report is unnumbered (report->id == 0),
hid_report_len(report) returns DIV_ROUND_UP(report->size, 8) without a report
ID byte. However, low-level raw_request implementations such as
usbhid_get_raw_report() and i2c_hid_get_raw_report() store 0 into buf[0],
advance buf++, and decrement count-- when reportnum == 0. Without allocating an
extra byte and stripping buf[0] before parsing (as __hid_request() in
drivers/hid/hid-core.c and vivaldi_feature_mapping() in
drivers/hid/hid-vivaldi-common.c do), the transfer buffer is 1 byte too short
(-EOVERFLOW or truncation) and hid_report_raw_event() parses the stuffed 0x00
byte at buf[0] as the first byte of the payload.

Third, hid_report_raw_event() only calls hid_process_report() when
hid->claimed != HID_CLAIMED_HIDRAW:

drivers/hid/hid-core.c:hid_report_raw_event() {
	if (hid->claimed != HID_CLAIMED_HIDRAW && report->maxfield) {
		hid_process_report(hid, report, cdata, interrupt);
		...
	}
}

Because the LampArray application usage (0x00590001) is not an input
application, a standalone LampArray device started via
hid_hw_start(hdev, HID_CONNECT_DEFAULT) without HID_CONNECT_DRIVER on non-USB
buses (or with CONFIG_USB_HIDDEV=n) has hdev->claimed == HID_CLAIMED_HIDRAW.
In that case hid_report_raw_event() skips hid_process_report(), leaving
ldev->lamp_count at 0 and failing with -EINVAL.

Would using kzalloc() (or hid_alloc_report_buf()) and hid_extract_bits() (or
handling the report->id == 0 offset and HID_CLAIMED_DRIVER requirement) avoid
these issues?

> +	if (ret < 0) {
> +		hid_warn(hdev, "Failed to parse LampArrayAttributesReport: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ldev->lamp_count_value = get_field_value(&ldev->lamp_count);
> +	if (!ldev->lamp_count_value) {
> +		hid_warn(hdev, "LampCount is 0 (invalid)\n");
> +		return -EINVAL;
> +	}
> +
> +	ldev->lamparray_kind_value = get_field_value(&ldev->lamparray_kind);
> +	return 0;
> +}
> +
> +static int lamparray_parse_update_report(struct lamparray_device *ldev)
> +{

[ ... ]

> +				} else if (collection_usage_id == HID_LAMP_RANGE_UPDATE_REPORT) {
> +					switch (id) {
> +					case HID_LAIP_RED_UPDATE_CHANNEL:
> +						ldev->color_update.red.field = field;
> +						ldev->color_update.red.index = j;
> +						break;
> +					case HID_LAIP_GREEN_UPDATE_CHANNEL:
> +						ldev->color_update.green.field = field;
> +						ldev->color_update.green.index = j;
> +						break;
> +					case HID_LAIP_BLUE_UPDATE_CHANNEL:
> +						ldev->color_update.blue.field = field;
> +						ldev->color_update.blue.index = j;
> +						break;
> +					case HID_LAIP_INTENSITY_UPDATE_CHANNEL:
> +						ldev->color_update.intensity.field = field;
> +						ldev->color_update.intensity.index = j;
> +						break;
> +					case HID_LAIP_LAMP_ID_START:
> +						ldev->range_start.field = field;
> +						ldev->range_start.index = j;
> +						break;
> +					case HID_LAIP_LAMP_ID_END:
> +						ldev->range_end.field = field;
> +						ldev->range_end.index = j;
> +						break;
> +					default:
> +						break;
> +					}
> +				} else if (collection_usage_id == HID_LAMPARRAY_CONTROL_REPORT &&
> +					   id == HID_LAIP_AUTONOMOUS_MODE) {
> +					ldev->autonomous_field.field = field;
> +					ldev->autonomous_field.index = j;
> +				}
> +			}
> +		}
> +	}
> +
> +	if (!ldev->autonomous_field.field ||
> +	    !lamparray_color_fields_is_complete(&ldev->color_update))
> +		ret = -ENODEV;
> +
> +	return ret;
> +}
> +
> +static int lamparray_hw_set_autonomous(struct lamparray_device *ldev,
> +				       bool enable)
> +{
> +	struct hid_device *hdev = ldev->hdev;
> +	struct hid_field *field = ldev->autonomous_field.field;
> +
> +	if (!field)
> +		return -ENODEV;
> +
> +	guard(mutex)(&ldev->dev_lock);
> +	set_field_value(&ldev->autonomous_field, !!enable);
> +	hid_hw_request(hdev, field->report, HID_REQ_SET_REPORT);
> +
> +	return 0;
> +}
> +
> +/* Make sure to hold dev lock to prevent concurrent access. */
> +static int lamparray_hw_set_state(struct lamparray_device *ldev, u8 r, u8 g,
> +				  u8 b, u8 intensity)
> +{
> +	struct hid_device *hdev = ldev->hdev;
> +	struct hid_report *report;
> +
> +	if (!lamparray_color_fields_is_complete(&ldev->color_update))
> +		return -ENODEV;
> +
> +	if (ldev->range_start.field && ldev->range_end.field) {
> +		set_field_value(&ldev->range_start, 0);
> +		set_field_value(&ldev->range_end, ldev->lamp_count_value - 1);
> +	}
> +
> +	set_field_value(&ldev->color_update.red, r);
> +	set_field_value(&ldev->color_update.green, g);
> +	set_field_value(&ldev->color_update.blue, b);
> +	set_field_value(&ldev->color_update.intensity, intensity);
> +
> +	report = ldev->color_update.red.field->report;
> +	hid_hw_request(hdev, report, HID_REQ_SET_REPORT);

[Severity: High]
Should lamparray_parse_update_report() and lamparray_hw_set_state() also parse
and set LampUpdateFlags (usage 0x0055) in LampRangeUpdateReport (0x0060)?

Per the HID Lighting and Illumination specification, LampRangeUpdateReport
contains LampUpdateFlags where bit 0 (LampUpdateComplete = 1) instructs the
device to commit buffered updates to the physical LEDs (as seen with
LAMP_UPDATE_FLAGS_LAMP_UPDATE_COMPLETE in
drivers/platform/x86/tuxedo/nb04/wmi_ab.c). Without setting LampUpdateFlags to
LampUpdateComplete before calling hid_hw_request(), won't spec-compliant
devices that buffer updates until LampUpdateComplete is asserted never apply
the color or brightness changes?

> +
> +	return 0;
> +}
> +
> +/*
> + * Simple helper to read the color information of the first lamp.
> + * This does not read the state of the whole lamp array since this driver only
> + * exposes one LED anyway, so one color is sufficient here for now.
> + */
> +static int lamparray_get_lamp_attributes(struct lamparray_device *ldev)
> +{
> +	struct hid_device *hdev = ldev->hdev;
> +	struct hid_report *report;
> +	int ret;
> +	size_t len;
> +
> +	if (!lamparray_color_fields_is_complete(&ldev->color_levels))
> +		return -ENODEV;
> +
> +	/*
> +	 * All four fields must share the same report since the
> +	 * attributes are fetched with a single GET_REPORT below.
> +	 */
> +	report = ldev->color_levels.red.field->report;
> +	if (!report ||
> +	    ldev->color_levels.green.field->report != report ||
> +	    ldev->color_levels.blue.field->report != report ||
> +	    ldev->color_levels.intensity.field->report != report)
> +		return -ENODEV;
> +
> +	len = hid_report_len(report);
> +	u8 *buf __free(kfree) = kmalloc(len, GFP_KERNEL);
> +	if (!buf)
> +		return -ENOMEM;
> +
> +	guard(mutex)(&ldev->dev_lock);
> +
> +	/* Update values */
> +	ret = hid_hw_raw_request(hdev, report->id, buf, len,
> +				 HID_FEATURE_REPORT, HID_REQ_GET_REPORT);

[Severity: Medium]
Does lamparray_get_lamp_attributes() need to send a LampAttributesRequestReport
(usage 0x0020) with LampId (0x0021) set to 0 before requesting
LampAttributesResponseReport (0x0022)?

In the HID Lighting and Illumination specification, reading
LampAttributesResponseReport returns the attributes of the lamp selected by a
preceding LampAttributesRequestReport and advances the device's internal LampId
cursor on each read (for example, handle_lamp_attributes_response_report() in
drivers/platform/x86/tuxedo/nb04/wmi_ab.c reads and increments
driver_data->next_lamp_id). If the device's internal LampId cursor was already
advanced by a prior probe, userspace hidraw access, or warm reboot, or if the
firmware requires an explicit request before populating the response report,
could this read attributes from a different lamp index or fail?

> +	if (ret < 0) {
> +		hid_warn(hdev, "Failed to request LampAttributesResponseReport: %d\n", ret);
> +		return ret;
> +	}

[ ... ]

> +static int lamparray_restore_state(struct lamparray_device *ldev)
> +{
> +	u8 r, g, b;
> +	int ret;
> +	enum led_brightness brightness;
> +
> +	if (!ldev->use_leds_uapi)
> +		return 0;
> +
> +	guard(mutex)(&ldev->dev_lock);
> +
> +	r = ldev->last_r;
> +	g = ldev->last_g;
> +	b = ldev->last_b;
> +	brightness = ldev->last_brightness;
> +
> +	ldev->mc_cdev.subled_info[0].intensity = r;
> +	ldev->mc_cdev.subled_info[1].intensity = g;
> +	ldev->mc_cdev.subled_info[2].intensity = b;
> +	ldev->mc_cdev.led_cdev.brightness = brightness;
> +
> +	/*
> +	 * Quirk for devices ignoring intensity values, see
> +	 * lamparray_led_brightness_set for more information.
> +	 */
> +	if (brightness)
> +		ret = lamparray_hw_set_state(ldev, r, g, b, brightness);
> +	else
> +		ret = lamparray_hw_set_state(ldev, 0, 0, 0, 0);
> +
> +	return ret;
> +}
> +
> +/* LEDs API */
> +
> +static int lamparray_led_brightness_set(struct led_classdev *cdev,
> +					enum led_brightness brightness)
> +{
> +	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> +	struct lamparray_device *ldev =
> +		container_of_const(mc, struct lamparray_device, mc_cdev);
> +	u8 r, g, b;
> +	int ret;
> +
> +	/*
> +	 * Brightness is handled by the LampArray device if supported,
> +	 * so we can pass the raw intensity values.
> +	 */
> +	r = mc->subled_info[0].intensity;
> +	g = mc->subled_info[1].intensity;
> +	b = mc->subled_info[2].intensity;
> +	mc->led_cdev.brightness = brightness;
> +
> +	guard(mutex)(&ldev->dev_lock);
> +
> +	/*
> +	 * Some devices completely ignore the intensity (brightness)
> +	 * value and instead rely on zeros for each RGB channel to turn
> +	 * the LEDs off (e.g. the ASUS Prime B650-Plus). Since it doesn't
> +	 * hurt spec-compliant devices to quirk this behavior, we turn
> +	 * off the RGB channels unconditionally when the brightness is
> +	 * zero.
> +	 */
> +	if (brightness)
> +		ret = lamparray_hw_set_state(ldev, r, g, b, brightness);
> +	else
> +		ret = lamparray_hw_set_state(ldev, 0, 0, 0, 0);
> +
> +	if (ret) {
> +		hid_err(ldev->hdev, "Failed to send LampArray update: %d\n",
> +			ret);
> +		return ret;
> +	}
> +
> +	ldev->last_r = r;
> +	ldev->last_g = g;
> +	ldev->last_b = b;
> +	ldev->last_brightness = brightness;
> +
> +	return 0;
> +}
> +
> +static enum led_brightness
> +lamparray_led_brightness_get(struct led_classdev *cdev)
> +{
> +	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> +	struct lamparray_device *ldev =
> +	    container_of_const(mc, struct lamparray_device, mc_cdev);
> +
> +	return ldev->last_brightness;
> +}

[Severity: Medium]
Can lamparray_led_brightness_get(), lamparray_led_brightness_set(), and
lamparray_restore_state() race with the LED core and corrupt the cached
brightness and subled intensities?

Because lamparray_register_led() sets cdev->brightness_set_blocking =
lamparray_led_brightness_set (with cdev->brightness_set = NULL), writes to
brightness or multi_intensity in sysfs call led_set_brightness(), which updates
led_cdev->brightness and schedules led_cdev->set_brightness_work on leds_wq.
However, ldev->last_brightness is only updated at the end of
lamparray_led_brightness_set() after hid_hw_request() completes.

If userspace reads the brightness sysfs attribute while set_brightness_work is
still queued or running, brightness_show() calls led_update_brightness():

drivers/leds/led-core.c:led_update_brightness() {
	if (led_cdev->brightness_get) {
		ret = led_cdev->brightness_get(led_cdev);
		if (ret >= 0) {
			led_cdev->brightness = ret;
			return 0;
		}
	}
}

This invokes lamparray_led_brightness_get(), which reads ldev->last_brightness
unlocked and overwrites led_cdev->brightness with the stale value. A subsequent
multi_intensity_store() then passes that stale led_cdev->brightness back to
led_set_brightness().

In addition, if software blinking (LED_BLINK_SW) is active,
led_timer_function() calls led_set_brightness_nosleep(led_cdev, LED_OFF) on the
off-phase, which runs lamparray_led_brightness_set(cdev, 0) and overwrites
ldev->last_brightness with 0.

Finally, lamparray_led_brightness_set() reads mc->subled_info[0..2].intensity
and writes mc->led_cdev.brightness before acquiring ldev->dev_lock, while
lamparray_restore_state() modifies ldev->mc_cdev.subled_info[0..2].intensity
and ldev->mc_cdev.led_cdev.brightness after the LED classdev is already
registered in sysfs without holding cdev->led_access, racing with concurrent
brightness_store() and multi_intensity_store() calls.

Since the hardware state is not read back in lamparray_led_brightness_get(), is
the brightness_get callback needed at all, and should subled_info and
brightness accesses be synchronized cleanly before or during LED registration?

> +
> +/* Make sure to hold sysfs lock to prevent concurrent access. */
> +static int lamparray_register_led(struct lamparray_device *ldev)
> +{
> +	struct device *dev = &ldev->hdev->dev;
> +	struct led_classdev *cdev = &ldev->mc_cdev.led_cdev;
> +	int ret;
> +
> +	if (ldev->led_registered)
> +		return 0;
> +
> +	if (!cdev->name) {
> +		/* Fallback value */
> +		const char *function = LED_FUNCTION_STATUS;
> +
> +		/* Some heuristics for choosing a better LED function. */
> +		if (ldev->lamparray_kind_value == HID_LAMPARRAY_KIND_KEYBOARD)
> +			function = LED_FUNCTION_KBD_BACKLIGHT;
> +
> +		cdev->name = kasprintf(GFP_KERNEL, "rgb:%s", function);
> +		if (!cdev->name)
> +			return -ENOMEM;
> +	}
> +
> +	mutex_lock(&ldev->dev_lock);
> +	/* Setup */
> +	cdev->max_brightness = ldev->max_brightness;
> +	cdev->brightness_set_blocking = lamparray_led_brightness_set;
> +	cdev->brightness_get = lamparray_led_brightness_get;
> +	/* Don't do anything automatically on shutdown or suspend. */
> +	cdev->flags |= LED_RETAIN_AT_SHUTDOWN;

[Severity: Medium]
When the LED classdev is unregistered and re-registered via
use_leds_uapi_store(), does cdev->flags retain LED_UNREGISTERING?

Writing 0 to use_leds_uapi calls:

use_leds_uapi_store()
  -> lamparray_unregister_led()
    -> led_classdev_multicolor_unregister(&ldev->mc_cdev)
      -> led_classdev_unregister(&mcled_cdev->led_cdev)

In drivers/leds/led-class.c, led_classdev_unregister() sets:

	led_cdev->flags |= LED_UNREGISTERING;

When 1 is later written to use_leds_uapi, lamparray_register_led() re-registers
the same embedded ldev->mc_cdev structure and only does
cdev->flags |= LED_RETAIN_AT_SHUTDOWN, leaving LED_UNREGISTERING permanently
set on the re-registered LED device. Should cdev->flags be assigned
(cdev->flags = LED_RETAIN_AT_SHUTDOWN) or have LED_UNREGISTERING cleared?

> +
> +	ldev->subleds[0].color_index = LED_COLOR_ID_RED;
> +	ldev->subleds[0].max_intensity = ldev->max_r;

[ ... ]

> +static ssize_t use_leds_uapi_store(struct device *dev,
> +				   struct device_attribute *attr,
> +				   const char *buf, size_t count)
> +{
> +	struct lamparray_device *ldev = lamparray_ldev_from_sysfs_dev(dev);
> +	bool val, old_val;
> +	int ret;
> +
> +	if (!ldev)
> +		return -ENODEV;
> +
> +	ret = kstrtobool(buf, &val);
> +	if (ret)
> +		return ret;
> +
> +	/* Make sure only one concurrent write is possible */
> +	guard(mutex)(&ldev->sysfs_lock);
> +	old_val = ldev->use_leds_uapi;
> +
> +	if (val == old_val)
> +		return count;
> +
> +	ldev->use_leds_uapi = val;
> +
> +	/*
> +	 * Take the device out of autonomous mode before exposing the LED
> +	 * class device, and hand it back afterwards, so that control is
> +	 * transferred in both directions rather than only at probe.
> +	 */
> +	if (val == 1) {
> +		ret = lamparray_hw_set_autonomous(ldev, false);
> +		if (ret)
> +			goto err_revert;
> +		ret = lamparray_register_led(ldev);
> +		if (ret) {
> +			lamparray_hw_set_autonomous(ldev, true);
> +			goto err_revert;
> +		}
> +		ret = lamparray_restore_state(ldev);
> +		if (ret) {
> +			hid_warn(ldev->hdev, "Could not restore state: %d\n", ret);
> +			lamparray_unregister_led(ldev);
> +			lamparray_hw_set_autonomous(ldev, true);
> +			goto err_revert;
> +		}
> +	} else {
> +		lamparray_unregister_led(ldev);
> +		ret = lamparray_hw_set_autonomous(ldev, true);
> +		if (ret) {
> +			hid_warn(ldev->hdev, "Could not enable autonomous mode: %d\n", ret);
> +			return count;
> +		}
> +	}
> +
> +	return count;
> +
> +err_revert:
> +	ldev->use_leds_uapi = old_val;
> +	return ret;
> +}

[Severity: Medium]
In use_leds_uapi_store() (which uses guard(mutex)(&ldev->sysfs_lock) together
with goto err_revert) and in lamparray_register() below (which uses
struct lamparray *la __free(kfree) together with goto err_remove_sysfs,
err_xa_erase, err_unregister_led, and err_free), this mixes scope-based cleanup
helpers with goto error paths in the same function.

Per include/linux/cleanup.h, goto and cleanup helpers should not be mixed in
the same routine. Could these functions be structured to avoid mixing
__free()/guard() with goto labels?

[ ... ]

> +struct lamparray *
> +lamparray_register(struct hid_device *hdev,
> +		   const struct lamparray_init_state *led_init_state)
> +{
> +	int ret;
> +	struct lamparray_device *ldev;
> +
> +	if (!hdev)
> +		return ERR_PTR(-ENODEV);
> +
> +	struct lamparray *la __free(kfree) = kzalloc_obj(*la, GFP_KERNEL);
> +	if (!la)
> +		return ERR_PTR(-ENOMEM);

[ ... ]

> +	/* No locking needed, no races are possible before sysfs setup. */
> +	ret = lamparray_register_led(ldev);
> +	if (ret) {
> +		hid_warn(hdev, "Failed to register LED UAPI: %d\n", ret);
> +		ldev->use_leds_uapi = false;
> +	}
> +
> +	ret = xa_err(xa_store(&lamparray_by_hdev, (unsigned long)hdev, ldev,
> +			      GFP_KERNEL));
> +	if (ret)
> +		goto err_unregister_led;
> +
> +	ret = lamparray_register_sysfs(ldev);
> +	if (ret)
> +		goto err_xa_erase;
> +
> +	ret = lamparray_hw_set_autonomous(ldev, false);
> +	if (ret) {
> +		hid_err(hdev, "Could not disable autonomous mode: %d", ret);
> +		goto err_remove_sysfs;
> +	}
> +
> +	hid_info(hdev, "LampArray device registered\n");
> +
> +	ret = lamparray_restore_state(ldev);
> +	if (ret) {
> +		hid_err(hdev, "Failed to set default state: %d", ret);
> +		goto err_remove_sysfs;
> +	}
> +
> +	return_ptr(la);
> +
> +err_remove_sysfs:
> +	lamparray_remove_sysfs(ldev);
> +err_xa_erase:
> +	xa_erase(&lamparray_by_hdev, (unsigned long)hdev);
> +err_unregister_led:
> +	mutex_lock(&ldev->sysfs_lock);
> +	lamparray_unregister_led(ldev);
> +	mutex_unlock(&ldev->sysfs_lock);
> +err_free:
> +	mutex_destroy(&ldev->dev_lock);
> +	mutex_destroy(&ldev->sysfs_lock);
> +	return ERR_PTR(ret);
> +}

[Severity: Medium]
Does the registration order in lamparray_register() expose the LED classdev and
use_leds_uapi sysfs attribute before the hardware is initialized?

Here, lamparray_register_led(), xa_store(), and lamparray_register_sysfs()
expose /sys/class/leds/rgb:<function> and
/sys/bus/hid/devices/<id>/use_leds_uapi before
lamparray_hw_set_autonomous(ldev, false) and lamparray_restore_state(ldev) are
called, and without holding ldev->sysfs_lock:

1. If userspace writes 0 to use_leds_uapi immediately after
   lamparray_register_sysfs(), use_leds_uapi_store() unregisters the LED and
   enables autonomous mode, and then lamparray_register() calls
   lamparray_hw_set_autonomous(ldev, false), leaving autonomous mode disabled
   while use_leds_uapi is 0. Likewise, lamparray_restore_state() checks
   !ldev->use_leds_uapi without holding sysfs_lock or dev_lock, and will
   overwrite any brightness or color set by udev or LED triggers after
   lamparray_register_led().

2. If lamparray_register_led() fails, ldev->use_leds_uapi is set to false, but
   lamparray_register() still calls lamparray_hw_set_autonomous(ldev, false)
   even though no LED class device was registered.

3. If lamparray_restore_state() fails after
   lamparray_hw_set_autonomous(ldev, false) succeeded, the err_remove_sysfs
   error path does not call lamparray_hw_set_autonomous(ldev, true) to restore
   autonomous mode.

Should autonomous mode be disabled and the initial state applied before
registering the LED classdev and sysfs attribute?

> +EXPORT_SYMBOL_GPL(lamparray_register);
> +
> +void lamparray_unregister(struct lamparray *la)
> +{
> +	struct lamparray_device *ldev;
> +
> +	if (!la)
> +		return;
> +
> +	ldev = &la->ldev;
> +
> +	lamparray_hw_set_autonomous(ldev, true);
> +
> +	lamparray_remove_sysfs(ldev);
> +	xa_erase(&lamparray_by_hdev, (unsigned long)ldev->hdev);
> +
> +	mutex_lock(&ldev->sysfs_lock);
> +	lamparray_unregister_led(ldev);
> +	mutex_unlock(&ldev->sysfs_lock);

[Severity: Medium]
In lamparray_unregister(), isn't the teardown order inverted relative to
use_leds_uapi_store()?

lamparray_hw_set_autonomous(ldev, true) is called before
lamparray_remove_sysfs(), xa_erase(), and lamparray_unregister_led(). While the
sysfs attribute and LED classdev are still registered, a concurrent write of 1
to use_leds_uapi_store() can disable autonomous mode again, or pending
cdev->set_brightness_work / blink timers (which are only flushed inside
led_classdev_unregister() during lamparray_unregister_led()) can run
lamparray_led_brightness_set() -> lamparray_hw_set_state() after autonomous
mode was re-enabled.

Also, because lamparray_hw_set_autonomous() submits HID_REQ_SET_REPORT via
hid_hw_request() (which is asynchronous on transports like usbhid) without
calling hid_hw_wait(ldev->hdev), if the caller invokes hid_hw_stop(hdev)
immediately after lamparray_unregister(), can usbhid_stop() cancel the
in-flight control request before autonomous mode is restored?

> +
> +	mutex_destroy(&ldev->dev_lock);
> +	mutex_destroy(&ldev->sysfs_lock);
> +	kfree(la);
> +}
> +EXPORT_SYMBOL_GPL(lamparray_unregister);

[ ... ]

> diff --git a/include/linux/hid-lamparray.h b/include/linux/hid-lamparray.h
> new file mode 100644
> index 0000000000000..6124878859392
> --- /dev/null
> +++ b/include/linux/hid-lamparray.h
> @@ -0,0 +1,89 @@

[ ... ]

> +/**
> + * lamparray_register() - initialize LampArray support for a HID device
> + * @hdev: HID device
> + * @led_init_state: Optional LED state at init specification
> + *
> + * Allocate and initialize internal LampArray state for the given HID device.
> + * The function parses required HID reports and fields and registers the
> + * associated miscdevice and sysfs attributes.
> + *
> + * Registers a multicolor LED class device to expose the LampArray functionality
> + * via the LED subsystem. If specified, the desired initial LED state is
> + * applied. If led_init_state is NULL, a default state is applied (white with
> + * zero brightness).
> + *
> + * Return: pointer to a LampArray handle on success, or ERR_PTR() on failure.
> + */
> +struct lamparray *lamparray_register(struct hid_device *hdev,
> +				     const struct lamparray_init_state *led_init_state);
> +
> +/**
> + * lamparray_unregister() - tear down LampArray support
> + * @la: LampArray handle returned by lamparray_register()
> + *
> + * Remove all resources associated with a LampArray instance.
> + *
> + * This unregisters the LED class device (if present), removes the miscdevice
> + * and sysfs interfaces and frees all internal state associated with @la.
> + */
> +void lamparray_unregister(struct lamparray *la);

[Severity: Low]
This isn't a bug, but the kernel-doc comments for lamparray_register() and
lamparray_unregister() mention registering and removing a miscdevice, which
does not exist in the implementation.

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261020-hid-lamparray-v7-0-47418b667b47@tuxedocomputers.com?part=1

  reply	other threads:[~2026-10-20 10:21 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-20  9:51 [PATCH v7 0/3] HID: generic: add LampArray support via hid-lamparray helper Aaron Erhardt
2026-10-20  9:51 ` [PATCH v7 1/3] HID: lamparray: add new LampArray helper module Aaron Erhardt
2026-10-20 10:21   ` sashiko-bot [this message]
2026-10-20  9:51 ` [PATCH v7 2/3] HID: generic: add LampArray support via hid-lamparray helper Aaron Erhardt
2026-10-20 10:06   ` sashiko-bot
2026-10-20  9:51 ` [PATCH v7 3/3] HID: lamparray: blank lamps across suspend and restore on resume Aaron Erhardt
2026-10-20 10:11   ` sashiko-bot

Reply instructions:

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

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

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

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

  git send-email \
    --in-reply-to=sashiko-outbox-178826@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=aer@tuxedocomputers.com \
    --cc=dmitry.torokhov@gmail.com \
    --cc=lee@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

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

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