syzbot
KASAN: slab-use-after-free Write in dev_config
Status:
upstream: reported on 2026/11/06 01:12
Subsystems:
usb
Labels:
prio:normal
actionable
[Documentation on labels]
Reported-by: syzbot+3a56b27661cb0e004a40@syzkaller.appspotmail.com
Fix commit:
cb3b441f2698 ("usb: gadget: fix refcount underflow in gadgetfs_bind() error path")
Patched on: [], missing on: [ci-qemu-gce-upstream-auto ci-qemu-native-arm64-kvm ci-qemu-upstream ci-qemu-upstream-386 ci-qemu2-arm32 ci-qemu2-arm64 ci-qemu2-arm64-compat ci-qemu2-arm64-mte ci-qemu2-riscv64 ci-snapshot-upstream-root ci-upstream-bpf-kasan-gce ci-upstream-bpf-next-kasan-gce ci-upstream-gce-arm64 ci-upstream-gce-leak ci-upstream-kasan-badwrites-root ci-upstream-kasan-gce ci-upstream-kasan-gce-386 ci-upstream-kasan-gce-root ci-upstream-kasan-gce-selinux-root ci-upstream-kasan-gce-smack-root ci-upstream-kmsan-gce-386-root ci-upstream-kmsan-gce-root ci-upstream-linux-next-kasan-gce-root ci-upstream-net-kasan-gce ci-upstream-net-this-kasan-gce ci-upstream-rust-kasan-gce ci2-upstream-fs ci2-upstream-kcsan-gce ci2-upstream-usb]
First crash: 47d, last: 47d
β¨ AI-generated root cause summary:
In drivers/usb/gadget/legacy/inode.c, gadgetfs_bind() only acquires a reference on struct dev_data (calling get_dev(dev)) at the very end of its success path (line 1709). If an error occurs earlier (such as in usb_ep_alloc_request() or activate_ep_files()), gadgetfs_bind() jumps to enomem and calls gadgetfs_unbind(), which unconditionally calls put_dev(dev) to release a reference that was never acquired. This reference count underflow prematurely frees struct dev_data via put_dev(). Furthermore, when driver probing fails during registration, usb_gadget_register_driver() still returns 0 because match_existing_only is not set on gadgetfs_driver, causing dev_config() to execute dev->gadget_registered = true on the already freed dev structure, resulting in a slab use-after-free write.
▶
▼
β¨ Full AI root cause analysis
Technical Root Cause Analysis
1. Involved Subsystems and Entities
- Source File:
drivers/usb/gadget/legacy/inode.c
- Functions:
dev_config(), gadgetfs_bind(), gadgetfs_unbind(), put_dev(), get_dev()
- Structs & Fields:
struct dev_data: holds gadgetfs device state, including count (refcount_t), state (enum ep0_state), buf (void *), and gadget_registered (bool).
gadgetfs_driver (struct usb_gadget_driver): legacy gadgetfs driver definition.
2. Root Cause Mechanism
The reported bug is a slab use-after-free write triggered when userspace writes configuration descriptors to /dev/gadget/$CHIP via dev_config():
-
Unbalanced Reference Dropping on gadgetfs_bind() Error:
In drivers/usb/gadget/legacy/inode.c:1678, gadgetfs_bind() binds the gadget driver to the USB device controller (UDC). When gadgetfs_bind() succeeds, it acquires a reference to struct dev_data by calling get_dev(dev) at line 1709:
INFO (dev, "bound to %s driver\n", gadget->name);
spin_lock_irq(&dev->lock);
dev->state = STATE_DEV_UNCONNECTED;
spin_unlock_irq(&dev->lock);
get_dev (dev);
return 0;
enomem:
gadgetfs_unbind (gadget);
return -ENOMEM;
If an error occurs before line 1709βsuch as a failure in usb_ep_alloc_request() (line 1697) or activate_ep_files() (line 1702)βexecution jumps to enomem: (line 1712), which invokes gadgetfs_unbind(gadget) at line 1713.
However, gadgetfs_unbind() unconditionally calls put_dev(dev) at line 1673:
static void
gadgetfs_unbind (struct usb_gadget *gadget)
{
...
destroy_ep_files (dev);
gadget->ep0->driver_data = NULL;
set_gadget_data (gadget, NULL);
if (dev->req)
usb_ep_free_request (gadget->ep0, dev->req);
DBG (dev, "%s done\n", __func__);
put_dev (dev);
}
Because get_dev(dev) was never called prior to this error exit, gadgetfs_unbind() drops a reference that was never acquired.
-
premature Freeing of struct dev_data:
When multiple UDCs match (e.g., gadget.0 and gadget.1), driver probe is attempted via bus_for_each_dev() in driver_register(). Each failed probe attempt calls gadgetfs_bind(), which through gadgetfs_unbind() drops an un-acquired reference. This decrements dev->count down to 0, causing put_dev() to call kfree(data):
static void put_dev (struct dev_data *data)
{
if (likely (!refcount_dec_and_test (&data->count)))
return;
BUG_ON (waitqueue_active (&data->wait));
kfree (data);
}
-
Driver Registration Returning 0 Despite Failed Bind:
In dev_config(), driver registration is initiated via:
value = usb_gadget_register_driver(&gadgetfs_driver);
Inside usb_gadget_register_driver_owner() (drivers/usb/gadget/udc/core.c:1752):
- Probing devices on the bus fails with
-ENOMEM (-12), logged as gadgetfs gadget.1: probe with driver gadgetfs failed with error -12.
- In Linux driver-core semantics, a probe failure does not make
driver_register() fail (it returns 0).
- When checking whether the driver is bound (
lines 1760β1770):
if (!driver->is_bound) {
if (driver->match_existing_only) {
pr_warn("%s: couldn't find an available UDC or it's busy\n",
driver->function);
ret = -EBUSY;
} else {
pr_info("%s: couldn't find an available UDC\n",
driver->function);
ret = 0;
}
}
Because gadgetfs_driver.match_existing_only is not set (false), the function logs UDC core: USB Gadget filesystem: couldn't find an available UDC and returns ret = 0.
-
Use-After-Free Write:
Because usb_gadget_register_driver() returns 0, dev_config() proceeds to the success branch (inode.c:1897-1909):
value = usb_gadget_register_driver(&gadgetfs_driver);
if (value != 0) {
spin_lock_irq(&dev->lock);
goto fail;
} else {
value = len;
dev->gadget_registered = true; /* <--- Use-after-free write */
}
At line 1908, it writes true (size 1) to dev->gadget_registered, but dev has already been freed by put_dev(), triggering the KASAN slab-use-after-free report.
Sample crash report:
* Struck through repros no longer work on HEAD.