Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
From: Alan Stern
Date: Mon Jul 20 2026 - 22:19:42 EST
On Mon, Jul 20, 2026 at 05:59:19AM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the revised patch and for your kind feedback on my
> analysis. I applied the patch to upstream v7.2-rc1, commit
> dc59e4fea9d83f03bad6bddf3fa2e52491777482.
>
> Regarding cancellation before ep_aio() returns, the reproducer I used
> for that test has separate submit and cancel threads. After
> kiocb_set_cancel_fn() links the corresponding aio_kiocb into
> ctx->active_reqs and releases ctx->ctx_lock, the cancel thread can find
> it and call ep_aio_cancel() before the submit thread returns from
> ep_aio(). The synchronous reference retained by the submission path is
> not dropped by io_submit_one() until after ep_aio() returns, so the
> aio_kiocb remains alive during this interval.
>
> With the revised patch, I did not observe the earlier setup-time
> null-ptr-deref or the original ep_aio_cancel() null-ptr-deref and UAF
> signatures.
>
> However, with kiocb_set_cancel_fn() moved after usb_ep_queue(), I
> observed a different UAF:
>
> BUG: KASAN: slab-use-after-free in kiocb_set_cancel_fn
> Write of size 8 in list_add_tail() at fs/aio.c:658
>
> After the request is queued by usb_ep_queue(), its completion callback
> can run before the iocb is added to active_reqs. In that ordering,
> aio_complete_rw() sees an empty ki_list, skips aio_remove_iocb(), and
> drops the asynchronous reference. ep_aio() subsequently links the
> completed iocb into active_reqs. When io_submit_one() drops the
> synchronous reference, the aio_kiocb can be freed while still linked. A
> later active-list operation can then access the stale entry; the first
> KASAN-detected invalid access occurred in list_add_tail() during a
> subsequent kiocb_set_cancel_fn() call.
>
> This UAF was repeatedly observed with the revised patch and was not
> observed in matched runs on the unpatched kernel using the same
> reproducer, workload, and timing settings.
I've got some ideas on how to fix these problems, but we should discuss
a few things first.
The real problem is that I don't have a clear idea of how the kernel's
aio implementation is meant to work, and it isn't documented at all. So
there are a lot of questions about how the cancel function is supposed
to interact with the normal I/O pathways.
For instance, what is the cancel function supposed to do if it is called
before the submission function (ep_aio() in this case) has returned, and
the submission function eventually returns something other than
-EIOCBQUEUED (i.e., submission failed and no I/O was started)? Or if it
is called before the submission function has started the actual I/O
operation, so there isn't anything to cancel yet?
What is the cancel function supposed to do if it is called after the I/O
has completed? Should it return an error code?
What is supposed to happen if the I/O is cancelled and then completes
with no errors?
How does the system handle a partial transfer, where the I/O was
cancelled part way through? How does the user learn how much data was
transferred before the I/O was cancelled?
I can program very defensively, to handle any possible interleaving of
the submission, completion, and cancellation paths, but I don't always
know what is the right thing to do in each case. I'd appreciate any
suggestions you can give.
Alan Stern