[PATCH v2 0/1] HID: wacom: fix sibling use-after-free in mode change

From: Jinmo Yang

Date: Sun Oct 04 2026 - 23:59:30 EST


Hi,

Please drop v1 and take this instead. v1 carries a Fixes tag and Cc: stable
but does not fix what it claims to, and it adds a second problem.

The Sashiko AI review of v1 is right: wacom_remove() stops the hardware and
cancels the delayed work before it takes the new mutex, so a sibling worker
holding that mutex can bring the device back up afterwards through
wacom_parse_and_register(), and removal never repeats those steps. Two
things escape:

- hidraw stays registered on an unbound device. hidraw_disconnect() is
reached only from hid_disconnect(), which is reached only from
hid_hw_stop(); neither hid_destroy_device() nor hid_remove_device()
touches it. That one is from reading the code.

- init_work is re-armed on memory devres is about to free, because
wacom_parse_and_register() calls wacom_query_tablet_data() ->
schedule_delayed_work(&wacom->init_work, 1s). That one KASAN catches.

Moving my test-only delay to just after the worker's hid_hw_stop(), so that
a concurrent wacom_remove() runs its own hid_hw_stop() inside it, gives this
on v1 five boots out of five:

BUG: KASAN: slab-use-after-free in __run_timer_base.part.0
Write of size 8 at addr ffff88800bc1b460 by task swapper/0/0
run_timer_softirq / handle_softirqs / sysvec_apic_timer_interrupt
Allocated by task 11: devm_kmalloc <- wacom_probe
Freed by task 79: devres_release_group <- hid_device_remove,
under uhid_char_release <- __x64_sys_close

1120 bytes into the freed object is inside wacom->init_work.timer
(offsetof(struct wacom, init_work) is 984 and its timer sits at +72 here,
from vmlinux DWARF).

My v1 testing missed all of this because the delay sat in
wacom_set_shared_values(), after the worker's hid_hw_start(), so the window
never opened. The clean v1 result was real but it was not testing this.

Changes in v2, all in wacom_remove(); the worker is unchanged:

- Take the mutex before hid_hw_close()/hid_hw_stop() rather than after,
and move the remaining cancel_*_work_sync() calls and
timer_delete_sync(&wacom->idleprox_timer) inside it, so the whole
teardown is covered.
- Keep cancel_work_sync(&wacom->mode_change_work) before the mutex, where
it has to be: a worker blocked on the mutex would deadlock it.
- Add a second cancel_work_sync(&wacom->mode_change_work) after the
unlock, because moving hid_hw_stop() inside the mutex lets a report
queue our own work again after the first cancel. That work can only find
a NULL wacom_wac.shared and return, and the mutex is free by then so it
cannot deadlock.

Same kernel and reproducer as v1 (x86_64, hid.git master, KASAN,
PROVE_LOCKING, one fresh QEMU guest per round). v2 is clean 10/10 with the
original delay placement and 5/5 with the one above, no lockdep report and
no leftover /sys/class/hidraw node; unpatched is 10/10 KASAN. The cost of
the global mutex and the untouched wacom_wireless_work() are as described
in v1 -- v2 only widens the hold to cover removal's own hid_hw_stop().

v1: https://lore.kernel.org/linux-input/20261004111353.118025-1-jinmo44.yang@xxxxxxxxx/
Sashiko: https://lore.kernel.org/linux-input/20261004112812.460B21F000FF@xxxxxxxxxxxxxxx/

Thanks,
Jinmo

Jinmo Yang (1):
HID: wacom: serialize mode changes with device removal

drivers/hid/wacom_sys.c | 60 ++++++++++++++++++++++++++++++++++-------
1 file changed, 50 insertions(+), 10 deletions(-)


base-commit: fe2ec83746e501645709761605c2464a44fd2929
--
2.53.0