Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()

From: Alan Stern

Date: Mon Aug 17 2026 - 22:57:33 EST


On Tue, Aug 18, 2026 at 04:49:08AM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for letting me know that the earlier results were helpful.
>
> I applied the patch as posted to upstream v7.2-rc1, commit
> dc59e4fea9d83f03bad6bddf3fa2e52491777482.
>
> I reran the original null-ptr-deref and UAF reproducers, along with the
> reproducers for the earlier candidate-patch regressions. I did not observe
> the corresponding KASAN signatures with this patch.

Nor any of the old lockdep violations, I trust.

> A separate cross-CPU concurrency diagnostic appears to expose a remaining
> lifetime race in the deferred-read cancellation path. As I understand it,
> copy_work and unlink_work are distinct work items queued through
> schedule_work() and may execute concurrently on different CPUs. I did not
> see an ordering mechanism in the posted patch that would serialize the two
> work items. I therefore treated their concurrent execution as a possible
> interleaving and modified only dummy_hcd to exercise it directly.

That's right; the two work items are allowed to run concurrently.

> On dummy_hcd's successful dequeue path, the calling path released
> dummy_hcd's lock and restored its IRQ state before invoking work_on_cpu().
> The diagnostic ran the giveback on another online CPU and waited for it to
> complete before usb_ep_dequeue() returned. GadgetFS remained exactly as in
> the posted patch. The target-CPU helper disabled local IRQs around
> usb_gadget_giveback_request() and restored the previous IRQ state after
> the function returned. The VM used a fixed four-CPU configuration and did
> not perform CPU hotplug.
>
> With this diagnostic, the C workload repeatedly triggered:
>
> BUG: KASAN: slab-use-after-free in ep_unlink_worker+0x1d1/0x1f0
> Read of size 8
> drivers/usb/gadget/legacy/inode.c:488
>
> The faulting source statement is:
>
> usb_ep_free_request(epdata->ep, priv->req);
>
> I did not reproduce this UAF with unmodified dummy_hcd under the same
> kernel configuration, reproducer, and arguments. Those runs completed
> without KASAN or an Oops and produced one AIO completion event with
> res=512 for each request.
>
> The observed UAF is consistent with the following ordering.
> ep_unlink_worker() calls usb_ep_dequeue(), and the giveback enters
> ep_aio_complete(), sets req_state to AIO_COMPLETED, and queues
> ep_user_copy_worker(). After usb_ep_dequeue() returns, the unlink worker
> observes AIO_COMPLETED, sets cancel_state to AIO_UNLINK_DONE, and releases
> aio_lock. The copy worker can then set req_state to AIO_GIVEN_BACK,
> observe that cancel_state is no longer AIO_UNLINKING, and free priv before
> the unlink worker reaches the access above.

Ah, that's an interleaving I failed to anticipate.

> aio_lock serializes these state changes, but it does not keep priv alive
> after the unlink worker releases the lock. This suggests that the final
> free must be deferred until every queued or running work item that may
> access priv has finished accessing it. For example, a lifetime reference
> could be taken for unlink_work before it is queued and released after
> ep_unlink_worker() has finished its final access, or an equivalent
> last-user mechanism could release priv only after both work items have
> finished accessing it.

It's an easy problem to fix; just make sure that the unlink worker does
not access priv after setting cancel_state to AIO_UNLINK_DONE unless it
sees that req_state was AIO_GIVEN_BACK. The revised patch is below.

> The reproducer does not close the GadgetFS files or trigger unbind during
> the request loop, and it consumes all AIO completion events before
> teardown. A workqueue flush in gadgetfs_unbind() would therefore address
> teardown ordering, but it would not serialize unlink_work and copy_work
> during this race.

Yes, I think that's an issue for a different discussion.

> If I have misunderstood whether the USB gadget API permits a UDC to
> deliver a dequeue giveback on another CPU before usb_ep_dequeue() returns,
> please let me know.

It all sounds good. There's just one more thing I'd like to be sure
gets tested: What happens if the aio is cancelled exactly between
ep_aio()'s calls to kiocb_set_cancel_fn() and usb_ep_queue()? This is a
new possibility created by the patch, and we should ensure that the
approach it takes is correct.

Thanks a lot for all your testing and analysis!

Alan Stern


Index: usb-devel/drivers/usb/gadget/legacy/inode.c
===================================================================
--- usb-devel.orig/drivers/usb/gadget/legacy/inode.c
+++ usb-devel/drivers/usb/gadget/legacy/inode.c
@@ -236,6 +236,8 @@ static void put_ep (struct ep_data *data
static const char *CHIP;
static DEFINE_MUTEX(sb_mutex); /* Serialize superblock operations */

+static DEFINE_SPINLOCK(aio_lock); /* Protect aio cancellation info */
+
/*----------------------------------------------------------------------*/

/* NOTE: don't use dev_printk calls before binding to the gadget
@@ -433,40 +435,99 @@ static long ep_ioctl(struct file *fd, un

/* ASYNCHRONOUS ENDPOINT I/O OPERATIONS (bulk/intr/iso) */

+enum aio_req_state {
+ AIO_SUBMITTING,
+ AIO_RUNNING,
+ AIO_COMPLETED,
+ AIO_GIVEN_BACK,
+};
+
+enum aio_cancel_state {
+ AIO_NOT_CANCELLED,
+ AIO_UNLINKING,
+ AIO_UNLINK_DONE,
+};
+
struct kiocb_priv {
struct usb_request *req;
struct ep_data *epdata;
struct kiocb *iocb;
struct mm_struct *mm;
- struct work_struct work;
+ struct work_struct copy_work;
+ struct work_struct unlink_work;
void *buf;
struct iov_iter to;
const void *to_free;
unsigned actual;
+ enum aio_req_state req_state;
+ enum aio_cancel_state cancel_state;
};

-static int ep_aio_cancel(struct kiocb *iocb)
+static void ep_unlink_worker(struct work_struct *work)
{
- struct kiocb_priv *priv = iocb->private;
+ struct kiocb_priv *priv;
struct ep_data *epdata;
- int value;
+ struct usb_request *req;
+ enum aio_req_state req_state;

- local_irq_disable();
+ priv = container_of(work, struct kiocb_priv, unlink_work);
epdata = priv->epdata;
- // spin_lock(&epdata->dev->lock);
- if (likely(epdata && epdata->ep && priv->req))
- value = usb_ep_dequeue (epdata->ep, priv->req);
- else
- value = -EINVAL;
- // spin_unlock(&epdata->dev->lock);
- local_irq_enable();
+ req = priv->req;

- return value;
+ usb_ep_dequeue(epdata->ep, req);
+
+ spin_lock_irq(&aio_lock);
+ req_state = priv->req_state;
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irq(&aio_lock);
+
+ /*
+ * req and epdata are freed after unlinking and completion are both done.
+ * priv is freed after unlinking and giveback are both done.
+ */
+ if (req_state >= AIO_COMPLETED) {
+ usb_ep_free_request(epdata->ep, req);
+ put_ep(epdata);
+ if (req_state == AIO_GIVEN_BACK)
+ kfree(priv);
+ }
+}
+
+static int ep_aio_cancel(struct kiocb *iocb)
+{
+ struct kiocb_priv *priv;
+
+ spin_lock_irq(&aio_lock);
+ priv = iocb->private;
+ if (!priv || priv->cancel_state != AIO_NOT_CANCELLED) {
+ spin_unlock_irq(&aio_lock);
+ return -EINVAL; /* Already completed or cancelled */
+ }
+ if (priv->req_state == AIO_SUBMITTING) {
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irq(&aio_lock);
+ return 0; /* ep_aio() will call us again if needed */
+ }
+
+ priv->cancel_state = AIO_UNLINKING;
+ spin_unlock_irq(&aio_lock);
+
+ /*
+ * We are called with the aio core holding iocb's context lock.
+ * usb_ep_dequeue() is allowed to run synchronously, calling the
+ * completion handler ep_aio_complete() before it returns.
+ * But ep_aio_complete() may call iocb->kio_complete(), which
+ * tries to acquire the context lock, leading to deadlock.
+ * For this reason, do the dequeue operation in a work routine.
+ */
+ INIT_WORK(&priv->unlink_work, ep_unlink_worker);
+ schedule_work(&priv->unlink_work);
+ return 0;
}

static void ep_user_copy_worker(struct work_struct *work)
{
- struct kiocb_priv *priv = container_of(work, struct kiocb_priv, work);
+ struct kiocb_priv *priv = container_of(work, struct kiocb_priv, copy_work);
struct mm_struct *mm = priv->mm;
struct kiocb *iocb = priv->iocb;
size_t ret;
@@ -482,7 +543,12 @@ static void ep_user_copy_worker(struct w

kfree(priv->buf);
kfree(priv->to_free);
- kfree(priv);
+
+ spin_lock_irq(&aio_lock);
+ priv->req_state = AIO_GIVEN_BACK;
+ if (priv->cancel_state != AIO_UNLINKING)
+ kfree(priv);
+ spin_unlock_irq(&aio_lock);
}

static void ep_aio_complete(struct usb_ep *ep, struct usb_request *req)
@@ -490,11 +556,13 @@ static void ep_aio_complete(struct usb_e
struct kiocb *iocb = req->context;
struct kiocb_priv *priv = iocb->private;
struct ep_data *epdata = priv->epdata;
+ enum aio_req_state new_req_state;
+ enum aio_cancel_state cancel_state;

- /* lock against disconnect (and ideally, cancel) */
- spin_lock(&epdata->dev->lock);
- priv->req = NULL;
- priv->epdata = NULL;
+ /* Prevent future cancellation */
+ spin_lock(&aio_lock);
+ iocb->private = NULL;
+ spin_unlock(&aio_lock);

/* if this was a write or a read returning no data then we
* don't need to copy anything to userspace, so we can
@@ -503,10 +571,9 @@ static void ep_aio_complete(struct usb_e
if (priv->to_free == NULL || unlikely(req->actual == 0)) {
kfree(req->buf);
kfree(priv->to_free);
- kfree(priv);
- iocb->private = NULL;
iocb->ki_complete(iocb,
req->actual ? req->actual : (long)req->status);
+ new_req_state = AIO_GIVEN_BACK;
} else {
/* ep_copy_to_user() won't report both; we hide some faults */
if (unlikely(0 != req->status))
@@ -515,13 +582,24 @@ static void ep_aio_complete(struct usb_e

priv->buf = req->buf;
priv->actual = req->actual;
- INIT_WORK(&priv->work, ep_user_copy_worker);
- schedule_work(&priv->work);
+ new_req_state = AIO_COMPLETED;
}

- usb_ep_free_request(ep, req);
- spin_unlock(&epdata->dev->lock);
- put_ep(epdata);
+ spin_lock(&aio_lock);
+ priv->req_state = new_req_state;
+ cancel_state = priv->cancel_state;
+ spin_unlock(&aio_lock);
+
+ if (new_req_state == AIO_COMPLETED) {
+ INIT_WORK(&priv->copy_work, ep_user_copy_worker);
+ schedule_work(&priv->copy_work);
+ }
+ if (cancel_state != AIO_UNLINKING) {
+ usb_ep_free_request(ep, req);
+ put_ep(epdata);
+ if (new_req_state == AIO_GIVEN_BACK)
+ kfree(priv);
+ }
}

static ssize_t ep_aio(struct kiocb *iocb,
@@ -532,11 +610,12 @@ static ssize_t ep_aio(struct kiocb *iocb
{
struct usb_request *req;
ssize_t value;
+ struct usb_ep *ep;
+ bool need_unlink = false;

iocb->private = priv;
priv->iocb = iocb;

- kiocb_set_cancel_fn(iocb, ep_aio_cancel);
get_ep(epdata);
priv->epdata = epdata;
priv->actual = 0;
@@ -547,10 +626,11 @@ static ssize_t ep_aio(struct kiocb *iocb
*/
spin_lock_irq(&epdata->dev->lock);
value = -ENODEV;
- if (unlikely(epdata->ep == NULL))
+ ep = epdata->ep;
+ if (unlikely(ep == NULL))
goto fail;

- req = usb_ep_alloc_request(epdata->ep, GFP_ATOMIC);
+ req = usb_ep_alloc_request(ep, GFP_ATOMIC);
value = -ENOMEM;
if (unlikely(!req))
goto fail;
@@ -560,12 +640,45 @@ static ssize_t ep_aio(struct kiocb *iocb
req->length = len;
req->complete = ep_aio_complete;
req->context = iocb;
- value = usb_ep_queue(epdata->ep, req, GFP_ATOMIC);
+
+ priv->req_state = AIO_SUBMITTING;
+ priv->cancel_state = AIO_NOT_CANCELLED;
+
+ /* Not allowed to manipulate the aio context while holding dev->lock */
+ ++epdata->dev->udc_usage;
+ spin_unlock_irq(&epdata->dev->lock);
+
+ kiocb_set_cancel_fn(iocb, ep_aio_cancel);
+ value = usb_ep_queue(ep, req, GFP_KERNEL);
+
+ spin_lock_irq(&epdata->dev->lock);
+ --epdata->dev->udc_usage;
+
if (unlikely(0 != value)) {
- usb_ep_free_request(epdata->ep, req);
+ spin_lock(&aio_lock);
+ iocb->private = NULL;
+ spin_unlock(&aio_lock);
+
+ usb_ep_free_request(ep, req);
goto fail;
}
spin_unlock_irq(&epdata->dev->lock);
+
+ spin_lock_irq(&aio_lock);
+ if (iocb->private != NULL) {
+ priv->req_state = AIO_RUNNING;
+
+ /* Cancelled before or just after submission? */
+ if (priv->cancel_state == AIO_UNLINK_DONE) {
+ priv->cancel_state = AIO_NOT_CANCELLED;
+ need_unlink = true;
+ }
+ } /* Otherwise already completed */
+ spin_unlock_irq(&aio_lock);
+
+ if (need_unlink) /* Redo cancel that was too early */
+ ep_aio_cancel(iocb);
+
return -EIOCBQUEUED;

fail: