[PATCH] HID: playstation: Fix DualShock4 dongle hotplug work use-after-free
From: Fan Wu <hidden>
Date: 2026-09-23 03:27:33
Also in:
lkml, stable
Subsystem:
hid core layer, hid playstation driver, the rest · Maintainers:
Jiri Kosina, Benjamin Tissoires, Roderick Colenbrander, Linus Torvalds
dualshock4_remove() cancels ds4->dongle_hotplug_worker while input
reports can still arrive: hid_hw_close() and hid_hw_stop() only run
later in ps_remove(), so a dongle connect report in that window makes
dualshock4_dongle_parse_report() schedule the just-cancelled work
again. ds4 is devm-managed and freed once ps_remove() returns, so
dualshock4_dongle_calibration_work() then runs on freed memory.
The dualshock4_create() error path has the same exposure. Reports are
already flowing when it runs, because ps_probe() starts and opens the
device before creating it, so the connect branch of the dongle report
handler can also queue ds4->output_worker through its lightbar update.
Neither work is cancelled when creation fails, and ds4 is freed once
the failed probe unwinds its devm allocations.
Fix this by clearing a new dongle_hotplug_worker_initialized flag under
ps_dev->lock before cancelling the worker and checking it under the same
lock before scheduling, mirroring how this driver already guards
ds4->output_worker. Also drain both workers on every error path of
dualshock4_create(): cancel dongle_hotplug_worker on the early MAC-read
and device-list failures that currently return directly, and clear
output_worker_initialized and cancel ds4->output_worker as
dualshock4_remove() already does. The output-report buffer allocation
failure returns through the same cleanup.
The locked check closes the schedule-vs-clear race, and neither work
re-schedules itself, so the cancels leave nothing pending.
This issue was found by an in-house static analysis tool.
Fixes: c64ed0cd9324 ("HID: playstation: add DualShock4 dongle support.")
Cc: stable@vger.kernel.org
Co-developed-by: Song Li <redacted>
Signed-off-by: Song Li <redacted>
Signed-off-by: Fan Wu <redacted>
---
drivers/hid/hid-playstation.c | 36 ++++++++++++++++++++++++++++-------
1 file changed, 29 insertions(+), 7 deletions(-)
diff --git a/drivers/hid/hid-playstation.c b/drivers/hid/hid-playstation.c
index f9dc937..1b7a18f 100644
--- a/drivers/hid/hid-playstation.c
+++ b/drivers/hid/hid-playstation.c@@ -421,6 +421,7 @@ struct dualshock4 { enum dualshock4_dongle_state dongle_state; /* Used during calibration. */ struct work_struct dongle_hotplug_worker; + bool dongle_hotplug_worker_initialized; /* Timestamp for sensor data */ bool sensor_timestamp_initialized;
@@ -2618,10 +2619,12 @@ static int dualshock4_dongle_parse_report(struct ps_device *ps_dev, struct hid_r dualshock4_set_default_lightbar_colors(ds4); - scoped_guard(spinlock_irqsave, &ps_dev->lock) + scoped_guard(spinlock_irqsave, &ps_dev->lock) { ds4->dongle_state = DONGLE_CALIBRATING; - schedule_work(&ds4->dongle_hotplug_worker); + if (ds4->dongle_hotplug_worker_initialized) + schedule_work(&ds4->dongle_hotplug_worker); + } /* Don't process the report since we don't have * calibration data, but let hidraw have it anyway.
@@ -2677,8 +2680,12 @@ static void dualshock4_remove(struct ps_device *ps_dev) cancel_work_sync(&ds4->output_worker); - if (ps_dev->hdev->product == USB_DEVICE_ID_SONY_PS4_CONTROLLER_DONGLE) + if (ps_dev->hdev->product == USB_DEVICE_ID_SONY_PS4_CONTROLLER_DONGLE) { + scoped_guard(spinlock_irqsave, &ds4->base.lock) + ds4->dongle_hotplug_worker_initialized = false; + cancel_work_sync(&ds4->dongle_hotplug_worker); + } } static inline void dualshock4_schedule_work(struct dualshock4 *ds4)
@@ -2770,12 +2777,15 @@ static struct ps_device *dualshock4_create(struct hid_device *hdev) max_output_report_size = sizeof(struct dualshock4_output_report_bt); ds4->output_report_dmabuf = devm_kzalloc(&hdev->dev, max_output_report_size, GFP_KERNEL); - if (!ds4->output_report_dmabuf) - return ERR_PTR(-ENOMEM); + if (!ds4->output_report_dmabuf) { + ret = -ENOMEM; + goto err_cancel; + } if (hdev->product == USB_DEVICE_ID_SONY_PS4_CONTROLLER_DONGLE) { ds4->dongle_state = DONGLE_DISCONNECTED; INIT_WORK(&ds4->dongle_hotplug_worker, dualshock4_dongle_calibration_work); + ds4->dongle_hotplug_worker_initialized = true; /* Override parse report for dongle specific hotplug handling. */ ps_dev->parse_report = dualshock4_dongle_parse_report;
@@ -2784,7 +2794,7 @@ static struct ps_device *dualshock4_create(struct hid_device *hdev) ret = dualshock4_get_mac_address(ds4); if (ret) { hid_err(hdev, "Failed to get MAC address from DualShock4\n"); - return ERR_PTR(ret); + goto err_cancel; } snprintf(hdev->uniq, sizeof(hdev->uniq), "%pMR", ds4->base.mac_address);
@@ -2796,7 +2806,7 @@ static struct ps_device *dualshock4_create(struct hid_device *hdev) ret = ps_devices_list_add(ps_dev); if (ret) - return ERR_PTR(ret); + goto err_cancel; ret = dualshock4_get_calibration_data(ds4); if (ret) {
@@ -2858,6 +2868,18 @@ static struct ps_device *dualshock4_create(struct hid_device *hdev) err: ps_devices_list_remove(ps_dev); +err_cancel: + scoped_guard(spinlock_irqsave, &ps_dev->lock) + ds4->output_worker_initialized = false; + + cancel_work_sync(&ds4->output_worker); + + if (ds4->dongle_hotplug_worker_initialized) { + scoped_guard(spinlock_irqsave, &ps_dev->lock) + ds4->dongle_hotplug_worker_initialized = false; + + cancel_work_sync(&ds4->dongle_hotplug_worker); + } return ERR_PTR(ret); }