Merge branch 'for-7.4/asus' into for-linus

- SG Mobile LED support (Denis Benato)
- power management fix for ROG Ally (Deni Benato)
- asus_work() hardening (Denis Benato)
- fix for invalid keyboard feature reports being sent on ASUS laptops
  with I2C/HID touchpads (Panz Dev)
This commit is contained in:
Jiri Kosina 2026-10-21 21:54:29 +02:00
commit 5ac62bdef7

View File

@ -51,6 +51,8 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define FEATURE_KBD_LED_REPORT_ID1 0x5d
#define FEATURE_KBD_LED_REPORT_ID2 0x5e
#define ROG_XGM_REPORT_SIZE 300
#define ROG_ALLY_REPORT_SIZE 64
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
@ -144,6 +146,11 @@ struct asus_worker {
bool removed;
};
struct asus_xgm_led {
struct led_classdev cdev;
struct hid_device *hdev;
};
struct asus_touchpad_info {
int max_x;
int max_y;
@ -170,6 +177,7 @@ struct asus_drvdata {
unsigned long battery_next_query;
struct asus_hid_listener listener;
bool fn_lock;
struct asus_xgm_led *xgm_led;
};
static int asus_report_battery(struct asus_drvdata *, u8 *, int);
@ -765,9 +773,14 @@ static void asus_work(struct work_struct *work)
struct asus_work_action *action = NULL;
unsigned long flags;
/* Save the action to be performed and clear the flag */
/*
* Dequeue the next action, if any. Once teardown has begun .removed
* is set and asus_worker_stop() drains the queue: leave the queued
* actions alone, they are dropped instead of being executed against
* a device that is being removed.
*/
spin_lock_irqsave(&worker->lock, flags);
if (!list_empty(&worker->actions)) {
if (!worker->removed && !list_empty(&worker->actions)) {
action = list_first_entry(&worker->actions,
struct asus_work_action, node);
list_del(&action->node);
@ -817,6 +830,25 @@ static int asus_worker_create(struct hid_device *hdev, struct asus_drvdata *drvd
return 0;
}
/**
* asus_worker_stop - quiesce the worker
* @worker: the worker to quiesce
*
* Once this function returns no more actions can be queued and no instance
* of asus_work() is running or pending.
*
* Callers must do this before hid_hw_stop(): actions are executed while the
* device is fully operational, since they send raw requests to it and, in
* the fan-key fallback path, re-inject raw reports into the HID core. After
* hid_hw_stop() the transport is gone (usbhid_stop() frees the URBs and the
* I/O buffers) and the input devices have been unregistered.
*
* The quiescing is race free because every site that queues an action holds
* worker->lock across the .removed check, the list insertion and
* schedule_work(): anything scheduled before .removed is set here is caught
* by the cancel_work_sync() below, anything after it is discarded by
* asus_worker_schedule().
*/
static void asus_worker_stop(struct asus_worker *worker)
{
struct asus_work_action *action, *tmp;
@ -1137,6 +1169,26 @@ static int asus_battery_probe(struct hid_device *hdev)
return ret;
}
static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value)
{
const u8 buf[ROG_XGM_REPORT_SIZE] = {
FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00
};
struct asus_xgm_led *xgm = container_of(led_cdev, struct asus_xgm_led, cdev);
int ret;
ret = asus_kbd_set_report(xgm->hdev, buf, ROG_XGM_REPORT_SIZE);
if (ret < 0) {
hid_err(xgm->hdev, "Unable to set XG mobile led state: %d\n", ret);
return ret;
} else if (ret != ROG_XGM_REPORT_SIZE) {
hid_err(xgm->hdev, "Unexpected partial transfer to XG mobile: %d\n", ret);
return -EIO;
}
return 0;
}
static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi)
{
struct input_dev *input = hi->input;
@ -1360,6 +1412,71 @@ static int asus_start_multitouch(struct hid_device *hdev)
return 0;
}
/*
* Initialize the reports of the device.
*
* Failures are intentionally not fatal: asus_kbd_init() tolerates a wrong
* handshake until this is verified to work for all devices, so a failure
* is only reported and the initialization of the remaining reports is
* still attempted.
*/
static void asus_initialize_reports(struct hid_device *hdev)
{
int ret;
for (int r = 0; r < ARRAY_SIZE(asus_report_id_init); r++) {
if (asus_has_report_id(hdev, asus_report_id_init[r])) {
ret = asus_kbd_init(hdev, asus_report_id_init[r]);
if (ret < 0)
hid_warn(hdev, "Failed to initialize 0x%x: %d.\n",
asus_report_id_init[r], ret);
}
}
}
static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
{
const char *name;
int ret;
drvdata->xgm_led = devm_kzalloc(&hdev->dev, sizeof(*drvdata->xgm_led), GFP_KERNEL);
if (drvdata->xgm_led == NULL)
return -ENOMEM;
name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s:led",
strlen(hdev->uniq) ? hdev->uniq : dev_name(&hdev->dev));
if (name == NULL) {
ret = -ENOMEM;
goto asus_xgm_init_err;
}
drvdata->xgm_led->hdev = hdev;
drvdata->xgm_led->cdev.name = name;
drvdata->xgm_led->cdev.brightness = 1;
drvdata->xgm_led->cdev.max_brightness = 1;
drvdata->xgm_led->cdev.brightness_set_blocking = asus_xgm_led_set;
drvdata->xgm_led->cdev.flags = LED_CORE_SUSPENDRESUME;
/* LED state is arbitrary on boot, set a default */
ret = asus_xgm_led_set(&drvdata->xgm_led->cdev, drvdata->xgm_led->cdev.brightness);
if (ret) {
hid_err(hdev, "Asus failed to set xgm led: %d\n", ret);
goto asus_xgm_init_err;
}
ret = devm_led_classdev_register(&hdev->dev, &drvdata->xgm_led->cdev);
if (ret) {
hid_err(hdev, "Asus failed to register xgm led: %d\n", ret);
goto asus_xgm_init_err;
}
return 0;
asus_xgm_init_err:
drvdata->xgm_led = NULL;
return ret;
}
static int __maybe_unused asus_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
@ -1379,6 +1496,9 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
if (!drvdata->tp)
asus_initialize_reports(hdev);
if (drvdata->tp)
return asus_start_multitouch(hdev);
@ -1493,12 +1613,16 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
return ret;
}
for (int r = 0; r < ARRAY_SIZE(asus_report_id_init); r++) {
if (asus_has_report_id(hdev, asus_report_id_init[r])) {
ret = asus_kbd_init(hdev, asus_report_id_init[r]);
if (ret < 0)
hid_warn(hdev, "Failed to initialize 0x%x: %d.\n",
asus_report_id_init[r], ret);
if (!drvdata->tp)
asus_initialize_reports(hdev);
if (asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID) &&
((hdev->product == USB_DEVICE_ID_ASUSTEK_XGM_2022) ||
(hdev->product == USB_DEVICE_ID_ASUSTEK_XGM_2023))) {
ret = asus_xgm_init(hdev, drvdata);
if (ret) {
hid_err(hdev, "Failed to initialize xg mobile: %d\n", ret);
goto err_stop_hw;
}
}
@ -1539,6 +1663,9 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
return ret;
@ -1551,6 +1678,9 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
}