Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
From: Minseo Kim
Date: Thu Jul 30 2026 - 12:29:00 EST
Hi Alan,
Sorry for the delayed response, and thank you for following up.
> What is the cancel function supposed to do if it is called after the I/O
> has completed? Should it return an error code?
My current view is yes. If "completed" means that AIO-core completion has
removed the iocb from ctx->active_reqs, a later cancellation should fail
because the request is no longer cancellable. It should not invoke
ep_aio_cancel(), touch the completed request or its private state, or
produce another completion event. The current implementation returns
-EINVAL in this case, although the exact userspace error to use is a
separate AIO-core/API question.
The distinction between AIO-core completion and completion of the
lower-level USB request is important here. If the USB request has
completed but the iocb is still present in ctx->active_reqs,
ep_aio_cancel() may still be called.
In that narrower interval, the completion-side flow already in progress,
including ep_user_copy_worker() where applicable, should remain the sole
source of the resulting AIO completion. The cancellation and completion
paths must coordinate any cleanup so that the GadgetFS private state and
USB request are each released exactly once. If usb_ep_dequeue() reports
that the request is no longer active, ep_aio_cancel() should propagate the
negative result without initiating a separate completion. In the current
implementation, io_cancel() still removes the iocb from ctx->active_reqs
as part of its own bookkeeping.
Conversely, if cancellation reaches an active USB request first and
usb_ep_dequeue() succeeds, the request is returned through the USB
completion/giveback path, which should remain the source of the resulting
AIO completion. The cancellation and completion paths must still
coordinate cleanup so that the GadgetFS private state and USB request are
each released exactly once; ep_aio_cancel() should not independently
generate another completion of the same iocb.
The behavior I observed is consistent with this exactly-once completion
and teardown requirement. After AIO-core completion, cancel attempts made
both before and after io_getevents() consumed the published event returned
-EINVAL and did not change or duplicate the event. In a deliberately
widened lower-level completion window, dummy_hcd returned -EINVAL because
the USB request was already inactive, and the normal AIO completion
remained the only event.
Your expectation about partial transfers also matched the observed
behavior: in both the PWRITE and PREADV checks, after a 512-byte partial
transfer, the single AIO completion event reported res=512 despite a
negative USB completion status.
For a fix, I think the important invariant is exactly-once completion and
teardown: each GadgetFS private object and USB request must be released
exactly once, the iocb must be completed exactly once, and no path may
dereference any of these objects after its lifetime has ended.
One additional point from the v2 results may be relevant to the approach
you outlined: delaying kiocb_set_cancel_fn() until after successful
submission. Moving it after a successful usb_ep_queue(), by itself, may
not be sufficient: once the request has been queued successfully, its
completion can run before cancellation is registered. The revised-patch
UAF demonstrated this completion-before-registration ordering. The
registration and completion paths therefore appear to need an additional
ordering or lifetime mechanism.
There is also a locking constraint: io_cancel() and free_ioctx_users() can
invoke the cancel callback while holding ctx->ctx_lock. A synchronous
dequeue giveback must therefore not cause aio_complete_rw() to reacquire
that lock while the iocb is still linked and the original caller still
holds ctx->ctx_lock.
I hope this answers the remaining semantic question and clarifies the
constraints that seem relevant to a revised fix.
Best regards,
Minseo Kim
2026년 7월 30일 (목) 오전 12:31, Alan Stern <stern@xxxxxxxxxxxxxxxxxxx>님이 작성:
>
> On Mon, Jul 20, 2026 at 10:18:33PM -0400, Alan Stern wrote:
> > I've got some ideas on how to fix these problems, but we should discuss
> > a few things first.
>
> Minseo, any responses to these questions?
>
> > 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?
>
> Never mind this question; I can avoid the issue by not calling
> kiocb_set_cancel_fn() until after the request has been sucessfully
> submitted.
>
> > 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?
>
> Presumably nothing special should happen in this case. It should look
> more or less the same as in the previous question.
>
> > 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?
>
> Presumably the second argument to ki_complete() contains this
> information.
>
> This leaves only one question for you to answer. :-)
>
> Alan Stern