[BUG] usb: gadget: u_serial: use-after-free of gserial in gs_write()/gs_flush_chars() remote-wakeup path

From: Yu Zhang

Date: Sun Oct 18 2026 - 23:37:18 EST


Hi,

gs_write() and gs_flush_chars() cache

struct gserial *gser = port->port_usb;

at function entry, before taking port->port_lock. In the
"if (port->suspended)" branch they drop port_lock and call
gserial_wakeup_host(gser) on that cached pointer, with no re-check of
port->port_usb -- unlike the sibling gs_start_tx() path in the very same
functions, which is guarded by "if (port->port_usb)" after re-acquiring
the lock.

gserial_wakeup_host() dereferences gser->func.config->cdev->gadget. A
concurrent gserial_disconnect() clears port->port_usb (under port_lock),
and the following unbind / function teardown frees the enclosing
struct f_acm / struct f_gser (which embeds the gserial) via ->free_func.
The cached gser is then used after free:

BUG: KASAN: slab-use-after-free in gs_write+0x120/0x240
Read of size 8 at addr ffff888006c5d430 by task ...
gs_write+0x120/0x240
n_tty_write+0x355/0xc30
file_tty_write.isra.0+...
vfs_write+...
ksys_write+...
Allocated by task ...:
acm_alloc_func+0x34/0x1f0
config_usb_cfg_link+0x15c/0x220
configfs_symlink+...
Freed by task ...:
kfree+...
acm_free_func+0x29/0x60
config_usb_cfg_unlink+0x1a9/0x240
configfs_unlink+...
__x64_sys_unlink+0x34/0x50

gs_flush_chars() has the identical shape.

== Reproduction ==

acm gadget on dummy_hcd (CONFIG_USB_DUMMY_HCD, CONFIG_USB_U_SERIAL,
CONFIG_USB_CONFIGFS_ACM), KASAN. Put the bus into suspend so
port->suspended is set: write "auto" to the gadget's host-side
.../power/control with autosuspend_delay_ms=0, which makes dummy_hcd
suspend the port and run the gadget ->suspend / gserial_suspend. Then:

- task A: write to /dev/ttyGS0 (enters the gs_write() suspended branch)
- task B: echo "" > g1/UDC ; rm g1/configs/c.1/acm.0 ; rmdir g1/functions/acm.0
(unbind + drop the acm function -> frees the gserial)

The gserial_wakeup_host(gser) call in task A then touches freed memory.
(I widened the window after the port_lock drop with a debug mdelay to make
it deterministic; without it this is a narrow race.)

The sibling gs_start_tx() call in the same two functions is guarded with
"if (port->port_usb)" after re-acquiring port_lock; the suspended/wakeup
branch dereferences the pre-lock cached gser with no equivalent check.