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

From: Minseo Kim

Date: Fri Sep 11 2026 - 10:28:02 EST


Hi Alan,

Thank you for the questions. I reran the tests to distinguish the
request's endpoint-queue state from the callback's execution state, and
the unbind flush from workqueue destruction during module cleanup.

> I didn't think this was possible. gadgetfs_unbind() calls
> destroy_ep_files() before doing the flush, and destroy_ep_files() calls
> usb_ep_disable() for each active endpoint. usb_ep_disable() doesn't
> return until the completion handlers for all the outstanding requests on
> the endpoint have finished. This means no copy_work items should have
> been left to queue when the flush occurred.

With both patches applied, this ordering was possible in the dummy_hcd
test because the target request had already been removed from its
endpoint queue before ep_aio_complete() ran. When gadgetfs_unbind()
subsequently called destroy_ep_files(), dummy_disable() found the
endpoint queue empty. The request associated with the already-running
callback was no longer on that queue, so nuke() had no queued request to
give back and dummy_disable() returned while the callback was still held
at the gate. The unbind flush then returned before the callback queued
copy_work.

I confirmed this with markers recording the request and endpoint
pointers. The gate was inside ep_aio_complete(), after the request had
already been removed from its endpoint queue. It used a bounded,
non-sleeping loop. dummy_hcd had already released dum->lock before
calling usb_gadget_giveback_request().

In a control with the host-side transfer delayed, the request remained
queued when usb_ep_disable() began. dummy_disable() called nuke(), which
removed it and invoked its completion callback through
usb_gadget_giveback_request(). While I held the callback at entry,
neither usb_ep_disable() nor the ep0 close returned. After I released the
gate, the callback returned before usb_ep_disable() did, matching your
expectation for this pending-request case. The AIO request produced
exactly one completion with res=-ESHUTDOWN.

In a separate run with GadgetFS still mounted, the unbind flush and ep0
close returned while the callback was held before queuing copy_work. The
reproducer had no GadgetFS file descriptors left open in userspace, but
the module usage count was 2. rmmod was rejected because gadgetfs was in
use, and gadgetfs_cleanup() had not begun. After I released the callback
and the reproducer exited, the count was 1; subsequent unmount and rmmod
succeeded.

> What prevented rmmod from completing after iocb->ki_complete() had
> finished? Was it waiting for the flush to finish? If any additional
> work items were added to the queue after the flush started, they should
> not have blocked the flush.

In a separate worker-tail run using the same late-queueing ordering, the
unbind flush had already returned before the callback queued copy_work.
I then held the worker immediately after iocb->ki_complete() returned.
The module usage count was zero before rmmod started. When rmmod was
started, it waited in destroy_workqueue() during module cleanup, not in
the earlier unbind flush. The relevant part of the blocked rmmod task's
stack was:

__flush_workqueue
drain_workqueue
destroy_workqueue
gadgetfs_cleanup [gadgetfs]
__do_sys_delete_module

The __flush_workqueue frame came from drain_workqueue(), which was
called by destroy_workqueue(). Thus, the late copy_work did not block
the earlier flush_workqueue() in gadgetfs_unbind(); it was already
running when gadgetfs_cleanup() called destroy_workqueue(), and
drain_workqueue() waited for it instead. Markers confirmed that
destroy_workqueue() returned only after I released the worker, and rmmod
then completed successfully.

This is consistent with the entry-gated ep_unlink_worker result in my
previous message. In that test, unlink_work had already been queued
before the unbind flush began, so the flush and the ep0 close remained
blocked until I released the worker. In the late-queueing run above,
copy_work was not queued until after the unbind flush had returned. A
separate control with copy_work queued before the flush likewise kept
the unbind flush and the ep0 close blocked until I released the worker.

Thank you for asking me to check this more closely.

With sincere appreciation and great respect,
Minseo Kim

2026년 9월 11일 (금) 오전 12:50, Alan Stern <stern@xxxxxxxxxxxxxxxxxxx>님이 작성:
>
> On Thu, Sep 10, 2026 at 11:00:00PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > Thank you for the second patch.
> >
> > > In your earlier testing, didn't you find that with the unpatched driver,
> > > the driver module's usage count remained elevated as long as the
> > > ep_user_copy_worker() was pending on the workqueue? And therefore it
> > > was impossible to unload the module while a work item was still queued
> > > or running?
> >
> > To check that point directly, I tested the unpatched upstream copy-work
> > path using diagnostic gates at two points in ep_user_copy_worker(). The
> > module usage count remained elevated when I held the worker at entry,
> > before iocb->ki_complete(). When I held the worker after that call, the
> > usage count reached zero and the module was unloaded before the worker
> > finished; the kernel then warned and panicked. This suggests that
> > AIO may not retain the file reference throughout the worker tail after
> > ki_complete().
>
> Okay, it's good to know that.
>
> > > Could you test it and verify that it prevents the problem you observed
> > > with only the first patch installed?
> >
> > Yes. I applied your first patch and then your second patch to upstream
> > v7.2-rc1, commit dc59e4fea9d83f03bad6bddf3fa2e52491777482.
> >
> > With the first patch alone, I reproduced the earlier module-unload
> > failure with ep_unlink_worker() held at entry. With both patches applied
> > using the same reproducer and ep_unlink_worker() entry gate,
> > gadgetfs_unbind() reached the flush point and the ep0 close remained
> > blocked, while unmount and rmmod attempts were rejected. After I released
> > the worker, the close completed and subsequent unmount and rmmod attempts
> > succeeded. Holding ep_user_copy_worker() at entry with both patches
> > applied produced the same result.
> >
> > I also tested the boundary where gadgetfs_unbind() reached the flush
> > point before the completion callback queued copy_work.
>
> I didn't think this was possible. gadgetfs_unbind() calls
> destroy_ep_files() before doing the flush, and destroy_ep_files() calls
> usb_ep_disable() for each active endpoint. usb_ep_disable() doesn't
> return until the completion handlers for all the outstanding requests on
> the endpoint have finished. This means no copy_work items should have
> been left to queue when the flush occurred.
>
> > After the
> > callback queued the work, I held the worker tail immediately after
> > iocb->ki_complete(). In this ordering, rmmod did not complete while the
> > worker was held; after I released the worker, rmmod completed cleanly.
>
> What prevented rmmod from completing after iocb->ki_complete() had
> finished? Was it waiting for the flush to finish? If any additional
> work items were added to the queue after the flush started, they should
> not have blocked the flush.
>
> > I reran the exact pre-queue cancellation matrix, the original
> > null-ptr-deref reproducer, the directed cross-CPU UAF reproducer,
> > deferred giveback, forced dequeue failure, the earlier NULL-endpoint
> > case, payload checks, and CPU hotplug. With both patches applied, all
> > produced the expected results without a KASAN report, Oops, or LOCKDEP
> > warning. Strict KCSAN did not report a race involving GadgetFS or its AIO
> > work functions.
> >
> > In my x86-64 QEMU and dummy_hcd tests, the second patch prevented the
> > module-unload problem that remained with the first patch alone. I did not
> > find a new failure attributable to the second patch in these tests.
>
> All right. So once these last few questions have been resolved, I will
> submit both of the patches (with your Tested-by: added to the second as
> well as the first).
>
> Thanks again for all your hard work running multiple tests.
>
> Alan Stern