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

104 views
Skip to first unread message

김민서

unread,
Jun 30, 2026, 7:08:50 PMJun 30
to Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
Hello,

I am reporting a USB gadgetfs AIO cancellation bug reproduced on upstream
v7.2-rc1, commit dc59e4fea9d83f03bad6bddf3fa2e52491777482, with KASAN
enabled. In my test environment, the reproducer repeatedly triggers a
KASAN null-ptr-deref/general protection fault in ep_aio_cancel(). I also
observed an intermittent slab-use-after-free at the same dereference site.

Target file:
drivers/usb/gadget/legacy/inode.c
Subsystem: USB gadgetfs
Git tree: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
Git head: dc59e4fea9d83f03bad6bddf3fa2e52491777482
Kernel release: v7.2-rc1

Observed crash:

KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f]
RIP: ep_aio_cancel+0x47/0xf0 drivers/usb/gadget/legacy/inode.c:455

Additional intermittent UAF observation:

BUG: KASAN: slab-use-after-free in ep_aio_cancel+0x25/0x90
drivers/usb/gadget/legacy/inode.c:455

Relevant stack:

ep_aio_cancel
__do_sys_io_cancel
__se_sys_io_cancel
__x64_sys_io_cancel
do_syscall_64
entry_SYSCALL_64_after_hwframe

Root cause analysis:

The crash appears to involve a race between gadgetfs AIO completion and
AIO cancellation.

In drivers/usb/gadget/legacy/inode.c, ep_aio_cancel() does:

struct kiocb_priv *priv = iocb->private;
...
epdata = priv->epdata;

At the same time, ep_aio_complete() can clear fields in the same private
object and then free it on the completion path before setting
iocb->private to NULL:

priv->req = NULL;
priv->epdata = NULL;
...
kfree(req->buf);
kfree(priv->to_free);
kfree(priv);
iocb->private = NULL;

My current understanding is that completion and cancellation are not
effectively serialized around this lifetime transition.

If cancellation observes NULL after iocb->private has been cleared,
ep_aio_cancel() dereferences priv->epdata through a NULL priv pointer and
reports the null-ptr-deref.

If cancellation observes the old non-NULL priv pointer after completion
has freed it, or if completion frees priv after cancellation loads it but
before cancellation reads priv->epdata, the same dereference can surface
as a slab-use-after-free.

Reproducer:

C reproducer:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_ep_aio_cancel_minimal_20260630/reproducers/repro_ndr_decoylock.c

Additional C reproducer used for the intermittent UAF observation:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_ep_aio_cancel_minimal_20260630/reproducers/repro_uaf_decoylock_107000_96000.c

Symbolized KASAN report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_ep_aio_cancel_minimal_20260630/reports/clean_report_ndr_inline.txt

Additional symbolized UAF report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_ep_aio_cancel_minimal_20260630/reports/clean_report_uaf_outline.txt

Kernel config:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_ep_aio_cancel_minimal_20260630/configs/kernel.config.v7.2-rc1-kasan-inline

Additional KASAN_OUTLINE config used for the intermittent UAF observation:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_ep_aio_cancel_minimal_20260630/configs/kernel.config.v7.2-rc1-kasan-outline

Build:

gcc -O2 -Wall -Wextra -pthread -o repro_ndr_decoylock repro_ndr_decoylock.c
gcc -O2 -Wall -Wextra -pthread -o repro_uaf_decoylock_107000_96000
repro_uaf_decoylock_107000_96000.c

Key config options:

CONFIG_USB_GADGETFS=y
CONFIG_USB_DUMMY_HCD=y
CONFIG_AIO=y
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y

Runtime conditions:

x86_64 QEMU/KVM
dummy_hcd + gadgetfs; physical USB hardware is not required
kasan_multi_shot=1
slub_debug=FZPU

The reproducer uses root only for the privileged environment setup
(dummy_hcd-backed gadgetfs setup, mounting gadgetfs, writing the initial
gadget descriptors/configuration through the control endpoint, and
preparing the endpoint file permissions). It then drops to uid/gid 1000
before opening/using the endpoint file(s) and triggering the AIO
completion/cancellation race. The faulting task in the null-ptr-deref
report is uid 1000.

Brief KASAN excerpt:

Oops: general protection fault, probably for non-canonical address
0xdffffc0000000001
KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f]
CPU: 2 UID: 1000 PID: 358 Comm: gdecoylock Not tainted 7.2.0-rc1
RIP: 0010:ep_aio_cancel+0x47/0xf0 drivers/usb/gadget/legacy/inode.c:455

Call Trace:
__do_sys_io_cancel
__se_sys_io_cancel
__x64_sys_io_cancel
do_syscall_64
entry_SYSCALL_64_after_hwframe

If you fix this issue, please add the following tag to the commit:

Reported-by: Minseo Kim <neck...@gmail.com>

If you need anything else, please let me know.

Best regards,
Minseo Kim

Alan Stern

unread,
Jun 30, 2026, 10:13:12 PMJun 30
to 김민서, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
Great! You clearly put a lot of work into finding this problem. How do
you suggest we fix it?

Alan Stern

김민서

unread,
Jul 2, 2026, 4:08:32 AMJul 2
to Alan Stern, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
Hi Alan,

Thank you for taking a look.

I do not want to over-specify the exact fix, but my tentative view from
the reproducer runs and code inspection is that the key invariant is that
struct kiocb_priv should either remain valid while it can still be reached
by ep_aio_cancel() for the same iocb, or it should be made unreachable from
the cancel path in a synchronized way.

In the immediate completion path I am looking at, ep_aio_complete()
appears to clear fields in priv and then free priv before iocb->private is
set to NULL and before iocb->ki_complete() runs. Meanwhile ep_aio_cancel()
reads iocb->private and immediately dereferences priv->epdata.

That seems to explain both symptoms I observed: a NULL-deref if
iocb->private is already NULL, and a UAF if cancellation observes a stale
non-NULL priv pointer.

I may be missing some ordering guarantee in the AIO core, but from the
reproducer this is the window that seems relevant. So I think a NULL check
in ep_aio_cancel() would likely handle the repeated NULL-deref symptom,
but may not address the stale non-NULL case. A safer direction might be to
ensure sufficient serialization or lifetime ordering around iocb->private /
struct kiocb_priv, so that either priv remains valid for the cancel
callback, or cancellation is already unreachable before priv is freed for
that iocb.

Best regards,
Minseo Kim

2026년 7월 1일 (수) 오전 11:13, Alan Stern <st...@rowland.harvard.edu>님이 작성:

Alan Stern

unread,
Jul 2, 2026, 10:22:58 AMJul 2
to 김민서, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
On Thu, Jul 02, 2026 at 05:08:17PM +0900, 김민서 wrote:
> Hi Alan,
>
> Thank you for taking a look.
>
> I do not want to over-specify the exact fix, but my tentative view from
> the reproducer runs and code inspection is that the key invariant is that
> struct kiocb_priv should either remain valid while it can still be reached
> by ep_aio_cancel() for the same iocb, or it should be made unreachable from
> the cancel path in a synchronized way.
>
> In the immediate completion path I am looking at, ep_aio_complete()
> appears to clear fields in priv and then free priv before iocb->private is
> set to NULL and before iocb->ki_complete() runs. Meanwhile ep_aio_cancel()
> reads iocb->private and immediately dereferences priv->epdata.
>
> That seems to explain both symptoms I observed: a NULL-deref if
> iocb->private is already NULL, and a UAF if cancellation observes a stale
> non-NULL priv pointer.
>
> I may be missing some ordering guarantee in the AIO core, but from the
> reproducer this is the window that seems relevant. So I think a NULL check
> in ep_aio_cancel() would likely handle the repeated NULL-deref symptom,
> but may not address the stale non-NULL case. A safer direction might be to
> ensure sufficient serialization or lifetime ordering around iocb->private /
> struct kiocb_priv, so that either priv remains valid for the cancel
> callback, or cancellation is already unreachable before priv is freed for
> that iocb.

Also, NULL checks don't fix races (cancel vs. complete).

It's hard to make lifetime ordering work when you don't know whether the
aio transfer will ever be cancelled. Serialization seems like the best
solution (provided you can guarantee that it won't cause a deadlock).

However, I believe there is a valid reason why the spinlock calls in
ep_aio_cancel() are commented out, although I don't remember what it is.
Maybe because at that point in the code there is no way to know whether
epdata and epdata->dev are valid pointers?

Anyway, it would be good if you could figure out a solution and write a
patch to implement it. Perhaps a single global spinlock to protect all
the aio pathways (or all those for a particular gadget) would work.

Alan Stern

김민서

unread,
Jul 5, 2026, 9:18:25 PMJul 5
to Alan Stern, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
Hi Alan,

Thank you, that makes sense.

> Also, NULL checks don't fix races (cancel vs. complete).

Agreed. A NULL check alone would likely only address the repeated
NULL-deref symptom, not the stale non-NULL priv case.

> Serialization seems like the best
> solution (provided you can guarantee that it won't cause a deadlock).

That makes sense to me. Locking changes in this path could affect USB
request completion, AIO completion, and callback/deadlock interactions, so
I do not think it would be appropriate for me to send a locking patch
myself without first being confident about the deadlock implications.

> Maybe because at that point in the code there is no way to know whether
> epdata and epdata->dev are valid pointers?

Yes, I see that concern. Simply restoring the commented-out spin_lock() in
ep_aio_cancel() does not look obviously safe to me either: ep_aio_cancel()
would first have to rely on iocb->private (the priv pointer) in order to
reach epdata and epdata->dev, and holding epdata->dev->lock around
usb_ep_dequeue() could have problematic interactions with the completion
path, since ep_aio_complete() appears to use the same lock.

I can help validate candidate fixes under KASAN against the reproducer and
the intermittent UAF variant. I can also build a lockdep-enabled kernel and
run the reproducer against candidate fixes if that would be useful.

The clearest conclusion I can draw is still the lifetime invariant:
ep_aio_cancel() should not be able to use iocb->private as a
struct kiocb_priv pointer after the completion path has cleared
iocb->private or freed the underlying struct kiocb_priv. I would be happy
to test any candidate fix or approach you think would be worth trying.

Best regards,
Minseo Kim

2026년 7월 2일 (목) 오후 11:22, Alan Stern <st...@rowland.harvard.edu>님이 작성:

Alan Stern

unread,
Jul 7, 2026, 1:31:28 PM (14 days ago) Jul 7
to 김민서, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
On Mon, Jul 06, 2026 at 10:18:11AM +0900, 김민서 wrote:
> I can help validate candidate fixes under KASAN against the reproducer and
> the intermittent UAF variant. I can also build a lockdep-enabled kernel and
> run the reproducer against candidate fixes if that would be useful.
>
> The clearest conclusion I can draw is still the lifetime invariant:
> ep_aio_cancel() should not be able to use iocb->private as a
> struct kiocb_priv pointer after the completion path has cleared
> iocb->private or freed the underlying struct kiocb_priv. I would be happy
> to test any candidate fix or approach you think would be worth trying.

Here is a possible fix for you to test. It's a little complicated,
which is to be expected since this is a somewhat complicated problem.

I believe it will solve the problems you are seeing but I have not tried
using it myself.

Alan Stern


---
drivers/usb/gadget/legacy/inode.c | 80 +++++++++++++++++++++++++++++---------
1 file changed, 63 insertions(+), 17 deletions(-)

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,6 +435,13 @@ static long ep_ioctl(struct file *fd, un

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

+enum aio_req_state {
+ AIO_REQ_RUNNING,
+ AIO_REQ_CANCELLING,
+ AIO_REQ_CANCELLED,
+ AIO_REQ_COMPLETED,
+};
+
struct kiocb_priv {
struct usb_request *req;
struct ep_data *epdata;
@@ -443,24 +452,46 @@ struct kiocb_priv {
struct iov_iter to;
const void *to_free;
unsigned actual;
+ enum aio_req_state aio_state;
};

static int ep_aio_cancel(struct kiocb *iocb)
{
- struct kiocb_priv *priv = iocb->private;
+ struct kiocb_priv *priv;
struct ep_data *epdata;
- int value;
+ struct usb_ep *ep;
+ struct dev_data *dev;
+ int value = -EINVAL;
+
+ spin_lock_irq(&aio_lock);
+ priv = iocb->private;
+ if (!priv || priv->aio_state != AIO_REQ_RUNNING)
+ goto Done; /* Already completed or cancelled */

- local_irq_disable();
+ priv->aio_state = AIO_REQ_CANCELLING;
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();
+ ep = epdata->ep;
+ dev = epdata->dev;
+ spin_lock(&dev->lock);
+ ++dev->udc_usage;
+ spin_unlock(&dev->lock);
+ spin_unlock_irq(&aio_lock);
+
+ value = usb_ep_dequeue(ep, priv->req);
+
+ spin_lock_irq(&aio_lock);
+ if (priv->aio_state == AIO_REQ_CANCELLING) {
+ priv->aio_state = AIO_REQ_CANCELLED;
+ } else { /* Must be AIO_REQ_COMPLETED */
+ usb_ep_free_request(ep, priv->req);
+ kfree(priv);
+ }
+ spin_lock(&dev->lock);
+ --dev->udc_usage;
+ spin_unlock(&dev->lock);

+ Done:
+ spin_unlock_irq(&aio_lock);
return value;
}

@@ -482,7 +513,13 @@ static void ep_user_copy_worker(struct w

kfree(priv->buf);
kfree(priv->to_free);
- kfree(priv);
+
+ spin_lock_irq(&aio_lock);
+ if (priv->aio_state == AIO_REQ_CANCELLING)
+ priv->aio_state = AIO_REQ_COMPLETED;
+ else
+ kfree(priv);
+ spin_unlock_irq(&aio_lock);
}

static void ep_aio_complete(struct usb_ep *ep, struct usb_request *req)
@@ -490,11 +527,15 @@ 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;
+ bool cancelling;
+
+ /* lock against cancellation */
+ spin_lock(&aio_lock);
+ cancelling = (priv->aio_state == AIO_REQ_CANCELLING);

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

/* 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,8 +544,10 @@ 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;
+ if (cancelling)
+ priv->aio_state = AIO_REQ_COMPLETED;
+ else
+ kfree(priv);
iocb->ki_complete(iocb,
req->actual ? req->actual : (long)req->status);
} else {
@@ -519,8 +562,10 @@ static void ep_aio_complete(struct usb_e
schedule_work(&priv->work);
}

- usb_ep_free_request(ep, req);
+ if (!cancelling)
+ usb_ep_free_request(ep, req);
spin_unlock(&epdata->dev->lock);
+ spin_unlock(&aio_lock);
put_ep(epdata);
}

@@ -541,6 +586,7 @@ static ssize_t ep_aio(struct kiocb *iocb
priv->epdata = epdata;
priv->actual = 0;
priv->mm = current->mm; /* mm teardown waits for iocbs in exit_aio() */
+ priv->aio_state = AIO_REQ_RUNNING;

/* each kiocb is coupled to one usb_request, but we can't
* allocate or submit those if the host disconnected.

Minseo Kim

unread,
Jul 13, 2026, 2:48:11 PM (8 days ago) Jul 13
to Alan Stern, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
Hi Alan,

Thank you very much for taking the time to prepare this patch. I applied
the patch as posted in your message to upstream v7.2-rc1, commit
dc59e4fea9d83f03bad6bddf3fa2e52491777482.

I repeatedly tested the proposed patch with the original null-ptr-deref
and UAF reproducers, as well as several reduced variants. During those
tests, I did not observe either of the original signatures.

While continuing the tests, I observed a new null-ptr-deref in
ep_aio_cancel() on the kernel with the proposed patch applied. It appears
to be associated with the initialization window in ep_aio(). Running the
reproducer without command-line arguments triggered:

KASAN: null-ptr-deref in range [0x0000000000000028-0x000000000000002f]
RIP: ep_aio_cancel+0xd0/0x2d0
drivers/usb/gadget/legacy/inode.c:473

The corresponding source statement is:

ep = epdata->ep;

where epdata is NULL. The faulting task has UID 1000.

One detail that may be relevant is the initialization order in ep_aio().
iocb->private is assigned and kiocb_set_cancel_fn() registers the
cancellation callback before priv->epdata is initialized. Because priv is
zero-initialized and AIO_REQ_RUNNING is the zero-valued enumerator in the
proposed patch, the state check can treat priv as RUNNING before the
explicit state assignment. The observed crash is consistent with
cancellation reaching this initialization window, because priv->epdata
was NULL at the fault.

I also ran the reproducer on a LOCKDEP-enabled build. LOCKDEP reported the
following possible circular locking dependency involving the newly added
aio_lock:

&ctx->ctx_lock -> aio_lock -> &dev->lock#2 -> &ctx->ctx_lock

The cycle appears to arise because io_cancel() calls ep_aio_cancel() while
holding ctx->ctx_lock, and ep_aio_cancel() then acquires aio_lock. In the
other direction, the patched completion path holds aio_lock and dev->lock
while calling iocb->ki_complete(). In this path, the completion callback
is aio_complete_rw(), which can acquire ctx->ctx_lock.

The reported cycle involving aio_lock appears specific to the proposed
patch, since aio_lock is newly introduced there.

Supporting files:

C reproducer for the new null-ptr-deref:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_validation_20260713/repro_candidate_setup_ndr.c

Build:
gcc -O2 -Wall -Wextra -pthread -o repro_candidate_setup_ndr
repro_candidate_setup_ndr.c

Clean symbolized report for the new null-ptr-deref:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_validation_20260713/clean_report_candidate_setup_ndr.txt

Kernel config used for the KASAN reproducer:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_validation_20260713/kernel.config.kasan_inline_dwarf5

LOCKDEP report for the aio_lock cycle:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_validation_20260713/lockdep_report_aio_lock_cycle.txt

I wanted to share these observations in case they are useful. Please let
me know if I have misunderstood any part of the initialization or locking
sequence.

Best regards,
Minseo Kim

2026년 7월 8일 (수) 오전 2:31, Alan Stern <st...@rowland.harvard.edu>님이 작성:

Alan Stern

unread,
Jul 13, 2026, 11:23:52 PM (7 days ago) Jul 13
to Minseo Kim, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
On Tue, Jul 14, 2026 at 03:47:55AM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you very much for taking the time to prepare this patch. I applied
> the patch as posted in your message to upstream v7.2-rc1, commit
> dc59e4fea9d83f03bad6bddf3fa2e52491777482.
>
> I repeatedly tested the proposed patch with the original null-ptr-deref
> and UAF reproducers, as well as several reduced variants. During those
> tests, I did not observe either of the original signatures.

That's good progress.

> While continuing the tests, I observed a new null-ptr-deref in
> ep_aio_cancel() on the kernel with the proposed patch applied. It appears
> to be associated with the initialization window in ep_aio(). Running the
> reproducer without command-line arguments triggered:
>
> KASAN: null-ptr-deref in range [0x0000000000000028-0x000000000000002f]
> RIP: ep_aio_cancel+0xd0/0x2d0
> drivers/usb/gadget/legacy/inode.c:473
>
> The corresponding source statement is:
>
> ep = epdata->ep;
>
> where epdata is NULL. The faulting task has UID 1000.
>
> One detail that may be relevant is the initialization order in ep_aio().
> iocb->private is assigned and kiocb_set_cancel_fn() registers the
> cancellation callback before priv->epdata is initialized. Because priv is
> zero-initialized and AIO_REQ_RUNNING is the zero-valued enumerator in the
> proposed patch, the state check can treat priv as RUNNING before the
> explicit state assignment. The observed crash is consistent with
> cancellation reaching this initialization window, because priv->epdata
> was NULL at the fault.

In the patch below, I moved the kiocb_set_cancel_fn() call down to after
all the other initialization. That ought to take care of the problem.
But I admit, I don't understand how the I/O can be cancelled before
ep_aio() returns.

> I also ran the reproducer on a LOCKDEP-enabled build. LOCKDEP reported the
> following possible circular locking dependency involving the newly added
> aio_lock:
>
> &ctx->ctx_lock -> aio_lock -> &dev->lock#2 -> &ctx->ctx_lock
>
> The cycle appears to arise because io_cancel() calls ep_aio_cancel() while
> holding ctx->ctx_lock, and ep_aio_cancel() then acquires aio_lock. In the
> other direction, the patched completion path holds aio_lock and dev->lock
> while calling iocb->ki_complete(). In this path, the completion callback
> is aio_complete_rw(), which can acquire ctx->ctx_lock.
>
> The reported cycle involving aio_lock appears specific to the proposed
> patch, since aio_lock is newly introduced there.

In the new patch I removed the dev->lock accesses in ep_aio_cancel() and
moved the iocb->ko_complete() call outside the scope of aio_lock in
ep_aio_complete(). This should break the locking cycles.

There's still a potential race: unbind against ep_aio_cancel or
ep_user_copy_work. That would be a difficult race to trigger; you'd
have to unbind the gadgetfs driver just as an aio request was
completing or being cancelled.

> I wanted to share these observations in case they are useful. Please let
> me know if I have misunderstood any part of the initialization or locking
> sequence.

No, I think you've done a great job of assimilating this complex
information. Let me know how the new patch works out.

Alan Stern


---
drivers/usb/gadget/legacy/inode.c | 92 +++++++++++++++++++++++++++++---------
1 file changed, 72 insertions(+), 20 deletions(-)

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,6 +435,13 @@ static long ep_ioctl(struct file *fd, un

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

+enum aio_req_state {
+ AIO_REQ_RUNNING,
+ AIO_REQ_CANCELLING,
+ AIO_REQ_CANCELLED,
+ AIO_REQ_COMPLETED,
+};
+
struct kiocb_priv {
struct usb_request *req;
struct ep_data *epdata;
@@ -443,24 +452,48 @@ struct kiocb_priv {
+ spin_unlock_irq(&aio_lock);
+
+ value = usb_ep_dequeue(ep, priv->req);
+
+ /*
+ * priv->req is freed by the first stage of completion or the
+ * second stage of cancellation, whichever is last.
+ * priv itself is freed by the final stage of completion or
+ * the second stage of cancellation, whichever is last.
+ */
+ spin_lock_irq(&aio_lock);
+ if (priv->aio_state == AIO_REQ_CANCELLING) {
+ /* Completion is not yet finished */
+ priv->aio_state = AIO_REQ_CANCELLED;
+ } else {
+ /* Must be AIO_REQ_COMPLETED so completion is finished */
+ usb_ep_free_request(ep, priv->req);
+ kfree(priv);
+ }

+ Done:
+ spin_unlock_irq(&aio_lock);
return value;
}

@@ -482,7 +515,13 @@ static void ep_user_copy_worker(struct w

kfree(priv->buf);
kfree(priv->to_free);
- kfree(priv);
+
+ spin_lock_irq(&aio_lock);
+ if (priv->aio_state == AIO_REQ_CANCELLING)
+ priv->aio_state = AIO_REQ_COMPLETED;
+ else
+ kfree(priv); /* Not cancelled or cancellation is finished */
+ spin_unlock_irq(&aio_lock);
}

static void ep_aio_complete(struct usb_ep *ep, struct usb_request *req)
@@ -490,11 +529,16 @@ 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;
+ bool cancelling;
+ size_t ret;
+
+ /* lock against cancellation */
+ spin_lock(&aio_lock);
+ cancelling = (priv->aio_state == AIO_REQ_CANCELLING);

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

/* 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,11 +547,17 @@ 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);
+ if (cancelling) /* Cancellation started, not yet finished */
+ priv->aio_state = AIO_REQ_COMPLETED;
+ else /* Not cancelled or cancellation is finished */
+ kfree(priv);
+ ret = req->actual;
+ if (ret == 0)
+ ret = req->status;
+ spin_unlock(&aio_lock); /* Cancel now may free req */
+ iocb->ki_complete(iocb, ret);
} else {
+ spin_unlock(&aio_lock);
/* ep_copy_to_user() won't report both; we hide some faults */
if (unlikely(0 != req->status))
DBG(epdata->dev, "%s fault %d len %d\n",
@@ -519,7 +569,8 @@ static void ep_aio_complete(struct usb_e
schedule_work(&priv->work);
}

- usb_ep_free_request(ep, req);
+ if (!cancelling) /* Not cancelled or cancellation is finished */
+ usb_ep_free_request(ep, req);
spin_unlock(&epdata->dev->lock);
put_ep(epdata);
}
@@ -536,11 +587,11 @@ static ssize_t ep_aio(struct kiocb *iocb
iocb->private = priv;
priv->iocb = iocb;

- kiocb_set_cancel_fn(iocb, ep_aio_cancel);
get_ep(epdata);
priv->epdata = epdata;
priv->actual = 0;
priv->mm = current->mm; /* mm teardown waits for iocbs in exit_aio() */
+ priv->aio_state = AIO_REQ_RUNNING;

/* each kiocb is coupled to one usb_request, but we can't
* allocate or submit those if the host disconnected.
@@ -566,6 +617,7 @@ static ssize_t ep_aio(struct kiocb *iocb
goto fail;
}
spin_unlock_irq(&epdata->dev->lock);
+ kiocb_set_cancel_fn(iocb, ep_aio_cancel);
return -EIOCBQUEUED;

fail:

Minseo Kim

unread,
Jul 19, 2026, 4:59:33 PM (2 days ago) Jul 19
to Alan Stern, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
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.

Although the direct aio_lock-to-ctx_lock nesting was removed from the
completion path, LOCKDEP still reported the following transitive cycle:

&ctx->ctx_lock -> aio_lock -> &dev->lock#2 -> &ctx->ctx_lock

In the immediate-completion branch, ep_aio_complete() acquires
dev->lock while holding aio_lock and later calls iocb->ki_complete()
while dev->lock remains held. If the iocb is still linked in
ctx->active_reqs, aio_complete_rw() calls aio_remove_iocb(), which
acquires ctx->ctx_lock. This preserves the
aio_lock -> dev->lock -> ctx_lock dependency, while io_cancel() supplies
the ctx_lock -> aio_lock edge.

I also exercised the unbind window you mentioned. Using
pointer-correlated tracing, I observed usb_ep_dequeue() from
ep_aio_cancel() for a given usb_request overlapping usb_ep_disable() in
the gadgetfs_unbind() path on that request's endpoint. In those
lineages, ep_aio_complete() and ep_user_copy_worker() also ran for the
same priv and usb_request. Neither those overlaps nor probe-disabled
runs covering the same timing ranges produced KASAN or an Oops. This
does not rule out an unbind-related failure at other timings; I did not
reproduce the suspected UAF in the timing ranges I tested.

Supporting files:

C reproducer for the post-queue registration UAF:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v2_validation_20260715/repro_v2_postqueue_uaf.c

Build:
gcc -O2 -Wall -Wextra -pthread -o repro_v2_postqueue_uaf \
repro_v2_postqueue_uaf.c

Symbolized KASAN report for the post-queue registration UAF:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v2_validation_20260715/symbolized_report_v2_postqueue_uaf.txt

Kernel config for the KASAN reproducer:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v2_validation_20260715/kernel.config.v7.2-rc1-candidate-v2-kasan-inline-dwarf5

LOCKDEP report for the remaining transitive cycle:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v2_validation_20260715/lockdep_v2_exact_cycle.txt

I hope this information is useful.

Best regards,
Minseo Kim

2026년 7월 14일 (화) 오후 12:23, Alan Stern <st...@rowland.harvard.edu>님이 작성:

Alan Stern

unread,
Jul 20, 2026, 10:18:41 PM (9 hours ago) Jul 20
to Minseo Kim, Greg Kroah-Hartman, linu...@vger.kernel.org, linux-...@vger.kernel.org, syzk...@googlegroups.com
On Mon, Jul 20, 2026 at 05:59:19AM +0900, Minseo Kim wrote:
> Hi Alan,
>
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
Reply all
Reply to author
Forward
0 new messages