* [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
@ 2026-06-30 23:08 김민서
2026-07-01 2:13 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: 김민서 @ 2026-06-30 23:08 UTC (permalink / raw)
To: Greg Kroah-Hartman; +Cc: linux-usb, linux-kernel, syzkaller
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 <neck3922@gmail.com>
If you need anything else, please let me know.
Best regards,
Minseo Kim
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-06-30 23:08 [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel() 김민서
@ 2026-07-01 2:13 ` Alan Stern
2026-07-02 8:08 ` 김민서
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-07-01 2:13 UTC (permalink / raw)
To: 김민서
Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
On Wed, Jul 01, 2026 at 08:08:34AM +0900, 김민서 wrote:
> 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.
Great! You clearly put a lot of work into finding this problem. How do
you suggest we fix it?
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-01 2:13 ` Alan Stern
@ 2026-07-02 8:08 ` 김민서
2026-07-02 14:22 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: 김민서 @ 2026-07-02 8:08 UTC (permalink / raw)
To: Alan Stern; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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 <stern@rowland.harvard.edu>님이 작성:
>
> On Wed, Jul 01, 2026 at 08:08:34AM +0900, 김민서 wrote:
> > 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.
>
> Great! You clearly put a lot of work into finding this problem. How do
> you suggest we fix it?
>
> Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-02 8:08 ` 김민서
@ 2026-07-02 14:22 ` Alan Stern
2026-07-06 1:18 ` 김민서
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-07-02 14:22 UTC (permalink / raw)
To: 김민서
Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-02 14:22 ` Alan Stern
@ 2026-07-06 1:18 ` 김민서
2026-07-07 17:31 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: 김민서 @ 2026-07-06 1:18 UTC (permalink / raw)
To: Alan Stern; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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 <stern@rowland.harvard.edu>님이 작성:
>
> 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
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-06 1:18 ` 김민서
@ 2026-07-07 17:31 ` Alan Stern
2026-07-13 18:47 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-07-07 17:31 UTC (permalink / raw)
To: 김민서
Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-07 17:31 ` Alan Stern
@ 2026-07-13 18:47 ` Minseo Kim
2026-07-14 3:23 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-07-13 18:47 UTC (permalink / raw)
To: Alan Stern; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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 <stern@rowland.harvard.edu>님이 작성:
>
> 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.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-13 18:47 ` Minseo Kim
@ 2026-07-14 3:23 ` Alan Stern
2026-07-19 20:59 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-07-14 3:23 UTC (permalink / raw)
To: Minseo Kim; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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 {
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_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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-14 3:23 ` Alan Stern
@ 2026-07-19 20:59 ` Minseo Kim
2026-07-21 2:18 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-07-19 20:59 UTC (permalink / raw)
To: Alan Stern; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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 <stern@rowland.harvard.edu>님이 작성:
>
> 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 {
> 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_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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-19 20:59 ` Minseo Kim
@ 2026-07-21 2:18 ` Alan Stern
2026-07-29 15:31 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-07-21 2:18 UTC (permalink / raw)
To: Minseo Kim; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-21 2:18 ` Alan Stern
@ 2026-07-29 15:31 ` Alan Stern
2026-07-30 15:55 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-07-29 15:31 UTC (permalink / raw)
To: Minseo Kim; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-29 15:31 ` Alan Stern
@ 2026-07-30 15:55 ` Minseo Kim
2026-07-31 18:59 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-07-30 15:55 UTC (permalink / raw)
To: Alan Stern; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
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@rowland.harvard.edu>님이 작성:
>
> 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
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-30 15:55 ` Minseo Kim
@ 2026-07-31 18:59 ` Alan Stern
2026-08-02 6:08 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-07-31 18:59 UTC (permalink / raw)
To: Minseo Kim; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
On Fri, Jul 31, 2026 at 12:55:03AM +0900, Minseo Kim wrote:
> 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.
Okay, the new patch below guarantees this. Or at least, I believe it
does. :-)
> 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.
The new patch registers the cancel function before submission, so this
will be okay.
> 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.
In the new patch, nothing more complicated than kfree() happens while
the new aio_lock is held, no new locking cycles will be created. Of
course, your testing may reveal a pre-existing cycle.
On the other hand, we have no control over whether dequeue givebacks are
synchronous. If necessary we could complete the aio in a different
thread, but that would be wasteful if it isn't needed.
Thanks for your help, and let me know how the patch below works out.
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,6 +435,19 @@ 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;
@@ -443,24 +458,53 @@ struct kiocb_priv {
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)
{
- struct kiocb_priv *priv = iocb->private;
+ struct kiocb_priv *priv;
struct ep_data *epdata;
+ struct usb_ep *ep;
int value;
+ enum aio_req_state req_state;
- local_irq_disable();
- 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();
+ 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 */
+ }
+ epdata = priv->epdata;
+ ep = epdata->ep;
+ priv->cancel_state = AIO_UNLINKING;
+ spin_unlock_irq(&aio_lock);
+
+ value = usb_ep_dequeue(ep, priv->req);
+
+ spin_lock_irq(&aio_lock);
+ req_state = priv->req_state;
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irq(&aio_lock);
+
+ /*
+ * priv->req and epdata are freed after unlinking and completion are
+ * both done.
+ * priv itself is freed after unlinking and giveback are both done.
+ */
+ if (req_state >= AIO_COMPLETED) {
+ usb_ep_free_request(ep, priv->req);
+ put_ep(epdata);
+ if (req_state == AIO_GIVEN_BACK)
+ kfree(priv);
+ }
return value;
}
@@ -482,7 +526,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 +539,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;
+ enum aio_req_state new_req_state;
+ enum aio_cancel_state cancel_state;
- /* lock against disconnect (and ideally, cancel) */
+ /* lock against unbind */
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 +557,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))
@@ -516,12 +569,23 @@ 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)
+ schedule_work(&priv->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 +596,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 +612,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 +626,37 @@ 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;
+ kiocb_set_cancel_fn(iocb, ep_aio_cancel);
+
+ value = usb_ep_queue(ep, req, GFP_ATOMIC);
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 (priv->req_state == AIO_SUBMITTING) {
+ 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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-07-31 18:59 ` Alan Stern
@ 2026-08-02 6:08 ` Minseo Kim
2026-08-02 16:07 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-08-02 6:08 UTC (permalink / raw)
To: Alan Stern; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the new patch. I applied it as posted to upstream v7.2-rc1,
commit dc59e4fea9d83f03bad6bddf3fa2e52491777482.
Across the timing ranges I tested, I did not reproduce the originally
reported ep_aio_cancel() stale-priv failure on the kernel with the new
patch applied. In a targeted diagnostic comparison, the original
ep_aio_cancel() UAF was reproduced on the unpatched kernel. Using the
same kernel configuration, reproducer, and arguments, the diagnostic
runs on the patched kernel produced no KASAN or Oops. I also did not
observe the original null-ptr-deref or UAF signatures in non-diagnostic
runs of the patched kernel.
However, I observed a different slab-use-after-free in ep_aio() on the
patched kernel:
BUG: KASAN: slab-use-after-free in ep_aio+0x573/0x660
Read of size 4
drivers/usb/gadget/legacy/inode.c:647
This UAF was repeatedly reproduced on the patched kernel. It was not
observed in control runs on the unpatched kernel using the same kernel
configuration, reproducer, workload, and timing settings.
The ordering appears to be:
1. ep_aio() registers cancellation and successfully queues the USB request.
2. ep_aio() releases epdata->dev->lock before acquiring aio_lock.
3. ep_aio_complete() can run the immediate-completion branch, clear
iocb->private, call iocb->ki_complete(), set priv->req_state to
AIO_GIVEN_BACK, observe that cancel_state is not AIO_UNLINKING, and
reach the path that frees priv.
4. ep_aio() then acquires aio_lock and reads priv->req_state at line 647.
Thus aio_lock serializes the post-queue state check and update, but it
does not keep priv alive until ep_aio() has finished that transition.
This is not the original ep_aio_cancel() stale-pointer window. The
observed UAF and the code ordering are consistent with ep_aio()
dereferencing priv after the completion path has freed it.
This suggests that the submitting path may need an additional lifetime
handoff. Either priv must remain alive until ep_aio() has finished its
post-queue work, or ep_aio() must no longer access priv after the point at
which the completion path may free it.
In diagnostic runs, I also tested cancellation while the request was in
AIO_SUBMITTING. In the queue-success ordering, I observed one completion
event with res=-ECONNRESET; in the forced queue-failure ordering, one
event with res=-EINVAL. Neither case produced KASAN or an Oops.
I also rechecked the potential unbind race you mentioned earlier. Using
pointer-correlated tracing on the patched build, I observed an
ep_aio_cancel() operation overlapping the gadgetfs_unbind() path while
that path called usb_ep_disable() on the request's endpoint. The
ep_aio_cancel() entry and ep_aio_complete() records referred to the same
iocb, priv, and usb_request, and the corresponding usb_ep_dequeue()
record referred to that same usb_request and endpoint.
ep_user_copy_worker() also ran for the same priv while gadgetfs_unbind()
was still in progress.
Corresponding runs with the probes disabled, using the same delay settings
as the traced runs, produced no KASAN or Oops.
Supporting files:
C reproducer for the ep_aio() post-queue UAF on the patched kernel:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_v3_validation_20260802/repro_v3_ep_aio_postqueue_uaf.c
Build:
gcc -O2 -Wall -Wextra -pthread -o repro_v3_ep_aio_postqueue_uaf \
repro_v3_ep_aio_postqueue_uaf.c
Symbolized KASAN report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_v3_validation_20260802/symbolized_report_v3_ep_aio_postqueue_uaf_unified.txt
Kernel config used for the post-queue UAF reproducer and unpatched
control runs:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_v3_validation_20260802/kernel.config.v7.2-rc1-candidate-v3-unified
I hope these results are useful.
Best regards,
Minseo Kim
2026년 8월 1일 (토) 오전 3:59, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Fri, Jul 31, 2026 at 12:55:03AM +0900, Minseo Kim wrote:
> > 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.
>
> Okay, the new patch below guarantees this. Or at least, I believe it
> does. :-)
>
> > 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.
>
> The new patch registers the cancel function before submission, so this
> will be okay.
>
> > 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.
>
> In the new patch, nothing more complicated than kfree() happens while
> the new aio_lock is held, no new locking cycles will be created. Of
> course, your testing may reveal a pre-existing cycle.
>
> On the other hand, we have no control over whether dequeue givebacks are
> synchronous. If necessary we could complete the aio in a different
> thread, but that would be wasteful if it isn't needed.
>
> Thanks for your help, and let me know how the patch below works out.
>
> 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,6 +435,19 @@ 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;
> @@ -443,24 +458,53 @@ struct kiocb_priv {
> 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)
> {
> - struct kiocb_priv *priv = iocb->private;
> + struct kiocb_priv *priv;
> struct ep_data *epdata;
> + struct usb_ep *ep;
> int value;
> + enum aio_req_state req_state;
>
> - local_irq_disable();
> - 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();
> + 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 */
> + }
>
> + epdata = priv->epdata;
> + ep = epdata->ep;
> + priv->cancel_state = AIO_UNLINKING;
> + spin_unlock_irq(&aio_lock);
> +
> + value = usb_ep_dequeue(ep, priv->req);
> +
> + spin_lock_irq(&aio_lock);
> + req_state = priv->req_state;
> + priv->cancel_state = AIO_UNLINK_DONE;
> + spin_unlock_irq(&aio_lock);
> +
> + /*
> + * priv->req and epdata are freed after unlinking and completion are
> + * both done.
> + * priv itself is freed after unlinking and giveback are both done.
> + */
> + if (req_state >= AIO_COMPLETED) {
> + usb_ep_free_request(ep, priv->req);
> + put_ep(epdata);
> + if (req_state == AIO_GIVEN_BACK)
> + kfree(priv);
> + }
> return value;
> }
>
> @@ -482,7 +526,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 +539,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;
> + enum aio_req_state new_req_state;
> + enum aio_cancel_state cancel_state;
>
> - /* lock against disconnect (and ideally, cancel) */
> + /* lock against unbind */
> 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 +557,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))
> @@ -516,12 +569,23 @@ 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)
> + schedule_work(&priv->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 +596,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 +612,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 +626,37 @@ 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;
> + kiocb_set_cancel_fn(iocb, ep_aio_cancel);
> +
> + value = usb_ep_queue(ep, req, GFP_ATOMIC);
> 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 (priv->req_state == AIO_SUBMITTING) {
> + 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:
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-02 6:08 ` Minseo Kim
@ 2026-08-02 16:07 ` Alan Stern
2026-08-04 23:12 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-08-02 16:07 UTC (permalink / raw)
To: Minseo Kim; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
On Sun, Aug 02, 2026 at 03:08:11PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the new patch. I applied it as posted to upstream v7.2-rc1,
> commit dc59e4fea9d83f03bad6bddf3fa2e52491777482.
>
> Across the timing ranges I tested, I did not reproduce the originally
> reported ep_aio_cancel() stale-priv failure on the kernel with the new
> patch applied. In a targeted diagnostic comparison, the original
> ep_aio_cancel() UAF was reproduced on the unpatched kernel. Using the
> same kernel configuration, reproducer, and arguments, the diagnostic
> runs on the patched kernel produced no KASAN or Oops. I also did not
> observe the original null-ptr-deref or UAF signatures in non-diagnostic
> runs of the patched kernel.
>
> However, I observed a different slab-use-after-free in ep_aio() on the
> patched kernel:
>
> BUG: KASAN: slab-use-after-free in ep_aio+0x573/0x660
> Read of size 4
> drivers/usb/gadget/legacy/inode.c:647
>
> This UAF was repeatedly reproduced on the patched kernel. It was not
> observed in control runs on the unpatched kernel using the same kernel
> configuration, reproducer, workload, and timing settings.
>
> The ordering appears to be:
>
> 1. ep_aio() registers cancellation and successfully queues the USB request.
> 2. ep_aio() releases epdata->dev->lock before acquiring aio_lock.
> 3. ep_aio_complete() can run the immediate-completion branch, clear
> iocb->private, call iocb->ki_complete(), set priv->req_state to
> AIO_GIVEN_BACK, observe that cancel_state is not AIO_UNLINKING, and
> reach the path that frees priv.
> 4. ep_aio() then acquires aio_lock and reads priv->req_state at line 647.
>
> Thus aio_lock serializes the post-queue state check and update, but it
> does not keep priv alive until ep_aio() has finished that transition.
> This is not the original ep_aio_cancel() stale-pointer window. The
> observed UAF and the code ordering are consistent with ep_aio()
> dereferencing priv after the completion path has freed it.
>
> This suggests that the submitting path may need an additional lifetime
> handoff. Either priv must remain alive until ep_aio() has finished its
> post-queue work, or ep_aio() must no longer access priv after the point at
> which the completion path may free it.
In fact it's simpler than that; I didn't think to add a test in one
spot. The updated patch is below. It differs from the previous patch
only in that one line.
> In diagnostic runs, I also tested cancellation while the request was in
> AIO_SUBMITTING. In the queue-success ordering, I observed one completion
> event with res=-ECONNRESET; in the forced queue-failure ordering, one
> event with res=-EINVAL. Neither case produced KASAN or an Oops.
>
> I also rechecked the potential unbind race you mentioned earlier. Using
> pointer-correlated tracing on the patched build, I observed an
> ep_aio_cancel() operation overlapping the gadgetfs_unbind() path while
> that path called usb_ep_disable() on the request's endpoint. The
> ep_aio_cancel() entry and ep_aio_complete() records referred to the same
> iocb, priv, and usb_request, and the corresponding usb_ep_dequeue()
> record referred to that same usb_request and endpoint.
> ep_user_copy_worker() also ran for the same priv while gadgetfs_unbind()
> was still in progress.
That's okay. The possibility I was worried about was that
gadgetfs_unbind() might complete while ep_aio_cancel() was still
running. If that happened, the usb_ep_free_request() and put_ep() calls
near the end of ep_aio_cancel() might run into trouble. Even worse, the
aio file descriptor might get closed and the entire module unloaded from
memory while ep_aio_cancel() is running on another CPU.
I don't know if that's possible, but I also don't see anything to
prevent it from happening. It might be necessary to add a reference
counter for the number of outstanding aio operations.
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,6 +435,19 @@ 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;
@@ -443,24 +458,53 @@ struct kiocb_priv {
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)
{
- struct kiocb_priv *priv = iocb->private;
+ struct kiocb_priv *priv;
struct ep_data *epdata;
+ struct usb_ep *ep;
int value;
+ enum aio_req_state req_state;
- local_irq_disable();
- 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();
+ 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 */
+ }
+ epdata = priv->epdata;
+ ep = epdata->ep;
+ priv->cancel_state = AIO_UNLINKING;
+ spin_unlock_irq(&aio_lock);
+
+ value = usb_ep_dequeue(ep, priv->req);
+
+ spin_lock_irq(&aio_lock);
+ req_state = priv->req_state;
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irq(&aio_lock);
+
+ /*
+ * priv->req and epdata are freed after unlinking and completion are
+ * both done.
+ * priv itself is freed after unlinking and giveback are both done.
+ */
+ if (req_state >= AIO_COMPLETED) {
+ usb_ep_free_request(ep, priv->req);
+ put_ep(epdata);
+ if (req_state == AIO_GIVEN_BACK)
+ kfree(priv);
+ }
return value;
}
@@ -482,7 +526,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 +539,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;
+ enum aio_req_state new_req_state;
+ enum aio_cancel_state cancel_state;
- /* lock against disconnect (and ideally, cancel) */
+ /* lock against unbind */
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 +557,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))
@@ -516,12 +569,23 @@ 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)
+ schedule_work(&priv->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 +596,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 +612,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 +626,37 @@ 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;
+ kiocb_set_cancel_fn(iocb, ep_aio_cancel);
+
+ value = usb_ep_queue(ep, req, GFP_ATOMIC);
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_SUBMITTING) {
+ 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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-02 16:07 ` Alan Stern
@ 2026-08-04 23:12 ` Minseo Kim
2026-08-05 16:17 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-08-04 23:12 UTC (permalink / raw)
To: Alan Stern; +Cc: Greg Kroah-Hartman, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the updated patch. I applied it as posted to upstream
v7.2-rc1, commit dc59e4fea9d83f03bad6bddf3fa2e52491777482.
In my runs, the post-queue ep_aio() UAF reported against the previous
revision did not recur with this update, including on a diagnostic build
that deliberately widened the same window. I also did not observe the
original ep_aio_cancel() null-ptr-deref or UAF signatures in the timings
I tested.
> The possibility I was worried about was that gadgetfs_unbind() might
> complete while ep_aio_cancel() was still running.
I was able to reach this ordering in a diagnostic build. I paused
ep_aio_cancel() after usb_ep_dequeue() returned and before the
cancel-side usb_ep_free_request() and put_ep() calls. The
pointer-correlated trace showed gadgetfs_unbind(),
usb_gadget_unregister_driver(), and dev_release() returning before those
cleanup calls and before ep_aio_cancel() returned.
No KASAN or Oops occurred under dummy_hcd. The trace establishes that,
in this instrumented run, the unbind and unregister paths returned before
the cancel-side cleanup finished.
> Even worse, the aio file descriptor might get closed and the entire
> module unloaded from memory while ep_aio_cancel() is running on another
> CPU.
I also tested this case with CONFIG_USB_GADGETFS=m. The only difference
between the built-in and modular test configurations was
CONFIG_USB_GADGETFS=y versus CONFIG_USB_GADGETFS=m. In a diagnostic run
with a longer pause, gadgetfs_unbind() returned while ep_aio_cancel() was
paused. At that point, the trace harness found no file descriptors
referring to /dev/gadget/* in the process's file descriptor table, and I
then started the unmount. While the unmount was still blocked and
ep_aio_cancel() had not returned, the reported reference count for the
gadgetfs module was 2 and an rmmod attempt failed with:
rmmod: ERROR: Module gadgetfs is in use
dev_release() and the unmount completed after ep_aio_cancel() returned,
and rmmod then succeeded. I therefore did not reproduce the module being
unloaded while ep_aio_cancel() was running in this path.
Several existing references are relevant here: the AIO request holds a
file reference in ki_filp until iocb_destroy(), the endpoint file
operations and GadgetFS filesystem type both specify
.owner = THIS_MODULE, and ep_aio() takes an ep_data reference through
get_ep(). The reference taken by get_ep() keeps ep_data alive at least
until its matching put_ep(); the final put_ep() also drops the dev_data
reference held by ep_data.
These references do not by themselves establish whether the UDC-provided
usb_ep object and the request allocated from it remain valid long enough
for the later cancel-side usb_ep_free_request() call to be safe after
gadgetfs_unbind() and usb_gadget_unregister_driver() have returned. The
outstanding-AIO reference count you suggested, or an equivalent teardown
barrier, may therefore still be relevant to this narrower lifetime
question, unless the USB core already guarantees those object lifetimes
through this ordering. Any such wait would need to avoid blocking a
disable or giveback operation needed for an outstanding AIO to finish.
Separately, in the matched LOCKDEP run I did not observe a cycle
involving aio_lock. The patched kernel reported:
&ctx->ctx_lock -> &dev->lock#2 -> &ctx->ctx_lock
In that report, one recorded dev->lock-to-ctx->ctx_lock dependency comes
from kiocb_set_cancel_fn() being called while dev->lock is held. The
reverse direction is exercised when free_ioctx_users() holds
ctx->ctx_lock and a synchronous dummy_hcd giveback enters
ep_aio_complete(), which acquires dev->lock. The cancel-function
registration placement was already present in the preceding patch
revision and was unchanged by the one-line update.
The matched unpatched control reported a direct recursive attempt to
acquire ctx->ctx_lock in the same synchronous-giveback call chain. I
therefore regard the synchronous ctx_lock re-entry itself as pre-existing
rather than as a regression introduced by the one-line update.
These reports confirm that, under dummy_hcd, the dequeue giveback can
re-enter the AIO completion path synchronously. They do not by themselves
establish that GadgetFS AIO completion must be deferred to another
thread, but they identify the synchronous callback chain that such
deferral would be intended to avoid. I have included both reports because
this ordering may also be relevant to the design of a teardown barrier.
Supporting files:
Unbind and cancel lifetime trace:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v4_validation_20260805/reports/unbind_cancel_lifetime_trace.txt
Module-unload lifetime trace:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v4_validation_20260805/reports/module_unload_lifetime_trace.txt
LOCKDEP report from the patched kernel:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v4_validation_20260805/reports/lockdep_report_candidate_v4.txt
Matched unpatched LOCKDEP control report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_v4_validation_20260805/reports/lockdep_report_unpatched_control.txt
I hope these results are useful.
Best regards,
Minseo Kim
2026년 8월 3일 (월) 오전 1:07, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Sun, Aug 02, 2026 at 03:08:11PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > Thank you for the new patch. I applied it as posted to upstream v7.2-rc1,
> > commit dc59e4fea9d83f03bad6bddf3fa2e52491777482.
> >
> > Across the timing ranges I tested, I did not reproduce the originally
> > reported ep_aio_cancel() stale-priv failure on the kernel with the new
> > patch applied. In a targeted diagnostic comparison, the original
> > ep_aio_cancel() UAF was reproduced on the unpatched kernel. Using the
> > same kernel configuration, reproducer, and arguments, the diagnostic
> > runs on the patched kernel produced no KASAN or Oops. I also did not
> > observe the original null-ptr-deref or UAF signatures in non-diagnostic
> > runs of the patched kernel.
> >
> > However, I observed a different slab-use-after-free in ep_aio() on the
> > patched kernel:
> >
> > BUG: KASAN: slab-use-after-free in ep_aio+0x573/0x660
> > Read of size 4
> > drivers/usb/gadget/legacy/inode.c:647
> >
> > This UAF was repeatedly reproduced on the patched kernel. It was not
> > observed in control runs on the unpatched kernel using the same kernel
> > configuration, reproducer, workload, and timing settings.
> >
> > The ordering appears to be:
> >
> > 1. ep_aio() registers cancellation and successfully queues the USB request.
> > 2. ep_aio() releases epdata->dev->lock before acquiring aio_lock.
> > 3. ep_aio_complete() can run the immediate-completion branch, clear
> > iocb->private, call iocb->ki_complete(), set priv->req_state to
> > AIO_GIVEN_BACK, observe that cancel_state is not AIO_UNLINKING, and
> > reach the path that frees priv.
> > 4. ep_aio() then acquires aio_lock and reads priv->req_state at line 647.
> >
> > Thus aio_lock serializes the post-queue state check and update, but it
> > does not keep priv alive until ep_aio() has finished that transition.
> > This is not the original ep_aio_cancel() stale-pointer window. The
> > observed UAF and the code ordering are consistent with ep_aio()
> > dereferencing priv after the completion path has freed it.
> >
> > This suggests that the submitting path may need an additional lifetime
> > handoff. Either priv must remain alive until ep_aio() has finished its
> > post-queue work, or ep_aio() must no longer access priv after the point at
> > which the completion path may free it.
>
> In fact it's simpler than that; I didn't think to add a test in one
> spot. The updated patch is below. It differs from the previous patch
> only in that one line.
>
> > In diagnostic runs, I also tested cancellation while the request was in
> > AIO_SUBMITTING. In the queue-success ordering, I observed one completion
> > event with res=-ECONNRESET; in the forced queue-failure ordering, one
> > event with res=-EINVAL. Neither case produced KASAN or an Oops.
> >
> > I also rechecked the potential unbind race you mentioned earlier. Using
> > pointer-correlated tracing on the patched build, I observed an
> > ep_aio_cancel() operation overlapping the gadgetfs_unbind() path while
> > that path called usb_ep_disable() on the request's endpoint. The
> > ep_aio_cancel() entry and ep_aio_complete() records referred to the same
> > iocb, priv, and usb_request, and the corresponding usb_ep_dequeue()
> > record referred to that same usb_request and endpoint.
> > ep_user_copy_worker() also ran for the same priv while gadgetfs_unbind()
> > was still in progress.
>
> That's okay. The possibility I was worried about was that
> gadgetfs_unbind() might complete while ep_aio_cancel() was still
> running. If that happened, the usb_ep_free_request() and put_ep() calls
> near the end of ep_aio_cancel() might run into trouble. Even worse, the
> aio file descriptor might get closed and the entire module unloaded from
> memory while ep_aio_cancel() is running on another CPU.
>
> I don't know if that's possible, but I also don't see anything to
> prevent it from happening. It might be necessary to add a reference
> counter for the number of outstanding aio operations.
>
> 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,6 +435,19 @@ 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;
> @@ -443,24 +458,53 @@ struct kiocb_priv {
> 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)
> {
> - struct kiocb_priv *priv = iocb->private;
> + struct kiocb_priv *priv;
> struct ep_data *epdata;
> + struct usb_ep *ep;
> int value;
> + enum aio_req_state req_state;
>
> - local_irq_disable();
> - 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();
> + 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 */
> + }
>
> + epdata = priv->epdata;
> + ep = epdata->ep;
> + priv->cancel_state = AIO_UNLINKING;
> + spin_unlock_irq(&aio_lock);
> +
> + value = usb_ep_dequeue(ep, priv->req);
> +
> + spin_lock_irq(&aio_lock);
> + req_state = priv->req_state;
> + priv->cancel_state = AIO_UNLINK_DONE;
> + spin_unlock_irq(&aio_lock);
> +
> + /*
> + * priv->req and epdata are freed after unlinking and completion are
> + * both done.
> + * priv itself is freed after unlinking and giveback are both done.
> + */
> + if (req_state >= AIO_COMPLETED) {
> + usb_ep_free_request(ep, priv->req);
> + put_ep(epdata);
> + if (req_state == AIO_GIVEN_BACK)
> + kfree(priv);
> + }
> return value;
> }
>
> @@ -482,7 +526,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 +539,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;
> + enum aio_req_state new_req_state;
> + enum aio_cancel_state cancel_state;
>
> - /* lock against disconnect (and ideally, cancel) */
> + /* lock against unbind */
> 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 +557,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))
> @@ -516,12 +569,23 @@ 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)
> + schedule_work(&priv->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 +596,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 +612,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 +626,37 @@ 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;
> + kiocb_set_cancel_fn(iocb, ep_aio_cancel);
> +
> + value = usb_ep_queue(ep, req, GFP_ATOMIC);
> 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_SUBMITTING) {
> + 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:
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-04 23:12 ` Minseo Kim
@ 2026-08-05 16:17 ` Alan Stern
2026-08-09 22:19 ` Andrey Konovalov
2026-08-14 16:17 ` Alan Stern
0 siblings, 2 replies; 45+ messages in thread
From: Alan Stern @ 2026-08-05 16:17 UTC (permalink / raw)
To: Minseo Kim, Greg Kroah-Hartman; +Cc: linux-usb, linux-kernel, syzkaller
On Wed, Aug 05, 2026 at 08:12:51AM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the updated patch. I applied it as posted to upstream
> v7.2-rc1, commit dc59e4fea9d83f03bad6bddf3fa2e52491777482.
>
> In my runs, the post-queue ep_aio() UAF reported against the previous
> revision did not recur with this update, including on a diagnostic build
> that deliberately widened the same window. I also did not observe the
> original ep_aio_cancel() null-ptr-deref or UAF signatures in the timings
> I tested.
But as you have seen, there are still other problems in that driver.
However, I'm not sure it's worth working on them. Greg KH is talking
about getting rid of almost all the drivers in the gadget/legacy
directory. I don't know if that will include the gadgetfs driver. If
it does, trying to fix up the driver will be a waste of time.
> > The possibility I was worried about was that gadgetfs_unbind() might
> > complete while ep_aio_cancel() was still running.
>
> I was able to reach this ordering in a diagnostic build. I paused
> ep_aio_cancel() after usb_ep_dequeue() returned and before the
> cancel-side usb_ep_free_request() and put_ep() calls. The
> pointer-correlated trace showed gadgetfs_unbind(),
> usb_gadget_unregister_driver(), and dev_release() returning before those
> cleanup calls and before ep_aio_cancel() returned.
>
> No KASAN or Oops occurred under dummy_hcd. The trace establishes that,
> in this instrumented run, the unbind and unregister paths returned before
> the cancel-side cleanup finished.
Yeah, that's not good. The unbind path, and in particular,
destroy_ep_files(), should wait until an endpoint being destroyed is no
longer in use.
> > Even worse, the aio file descriptor might get closed and the entire
> > module unloaded from memory while ep_aio_cancel() is running on another
> > CPU.
>
> I also tested this case with CONFIG_USB_GADGETFS=m. The only difference
> between the built-in and modular test configurations was
> CONFIG_USB_GADGETFS=y versus CONFIG_USB_GADGETFS=m. In a diagnostic run
> with a longer pause, gadgetfs_unbind() returned while ep_aio_cancel() was
> paused. At that point, the trace harness found no file descriptors
> referring to /dev/gadget/* in the process's file descriptor table, and I
> then started the unmount. While the unmount was still blocked and
> ep_aio_cancel() had not returned, the reported reference count for the
> gadgetfs module was 2 and an rmmod attempt failed with:
>
> rmmod: ERROR: Module gadgetfs is in use
That's a relief.
> dev_release() and the unmount completed after ep_aio_cancel() returned,
> and rmmod then succeeded. I therefore did not reproduce the module being
> unloaded while ep_aio_cancel() was running in this path.
>
> Several existing references are relevant here: the AIO request holds a
> file reference in ki_filp until iocb_destroy(), the endpoint file
> operations and GadgetFS filesystem type both specify
> .owner = THIS_MODULE, and ep_aio() takes an ep_data reference through
> get_ep(). The reference taken by get_ep() keeps ep_data alive at least
> until its matching put_ep(); the final put_ep() also drops the dev_data
> reference held by ep_data.
The ep_data and dev_data references don't help pin the module, but the
ki_filp reference does.
On the other hand, if ep_cancel() gets split into two threads (which
will be necessary to eliminate the locking cycle mentioned below), its
second half could end up running after iocb_destroy() is finished. This
is yet another reason for making destroy_ep_files() wait until the
endpoint is no longer in use.
> These references do not by themselves establish whether the UDC-provided
> usb_ep object and the request allocated from it remain valid long enough
> for the later cancel-side usb_ep_free_request() call to be safe after
> gadgetfs_unbind() and usb_gadget_unregister_driver() have returned. The
> outstanding-AIO reference count you suggested, or an equivalent teardown
> barrier, may therefore still be relevant to this narrower lifetime
> question, unless the USB core already guarantees those object lifetimes
> through this ordering. Any such wait would need to avoid blocking a
> disable or giveback operation needed for an outstanding AIO to finish.
AIO doesn't wait for disable operations, only giveback. I don't think
this will cause any new problems.
> Separately, in the matched LOCKDEP run I did not observe a cycle
> involving aio_lock. The patched kernel reported:
>
> &ctx->ctx_lock -> &dev->lock#2 -> &ctx->ctx_lock
>
> In that report, one recorded dev->lock-to-ctx->ctx_lock dependency comes
> from kiocb_set_cancel_fn() being called while dev->lock is held. The
> reverse direction is exercised when free_ioctx_users() holds
> ctx->ctx_lock and a synchronous dummy_hcd giveback enters
> ep_aio_complete(), which acquires dev->lock. The cancel-function
> registration placement was already present in the preceding patch
> revision and was unchanged by the one-line update.
Fixing this will require moving ep_aio()'s calls to
kiocb_set_cancel_fn() and usb_ep_queue() outside the scope of dev->lock.
There are several other places in the driver where the code does
something similar, so this should be straightforward.
> The matched unpatched control reported a direct recursive attempt to
> acquire ctx->ctx_lock in the same synchronous-giveback call chain. I
> therefore regard the synchronous ctx_lock re-entry itself as pre-existing
> rather than as a regression introduced by the one-line update.
To fix this, I will have to put the second half of ep_cancel()
(everything from the usb_ep_dequeue() call to the end) into a workqueue
routine, rather like ep_user_copy_worker(). And to avoid other locking
cycles, the rule should be to allow dev->lock to be acquired while
ctx->ctx_lock is held, but not the reverse.
> These reports confirm that, under dummy_hcd, the dequeue giveback can
> re-enter the AIO completion path synchronously. They do not by themselves
> establish that GadgetFS AIO completion must be deferred to another
> thread, but they identify the synchronous callback chain that such
> deferral would be intended to avoid. I have included both reports because
> this ordering may also be relevant to the design of a teardown barrier.
Yes, thank you, it did help.
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-05 16:17 ` Alan Stern
@ 2026-08-09 22:19 ` Andrey Konovalov
2026-08-14 16:17 ` Alan Stern
1 sibling, 0 replies; 45+ messages in thread
From: Andrey Konovalov @ 2026-08-09 22:19 UTC (permalink / raw)
To: Alan Stern, Greg Kroah-Hartman
Cc: Minseo Kim, linux-usb, linux-kernel, syzkaller
On Wed, Aug 5, 2026 at 6:17 PM Alan Stern <stern@rowland.harvard.edu> wrote:
>
> However, I'm not sure it's worth working on them. Greg KH is talking
> about getting rid of almost all the drivers in the gadget/legacy
> directory. I don't know if that will include the gadgetfs driver. If
> it does, trying to fix up the driver will be a waste of time.
Hi Alan and Greg,
Gonna hijack the discussion here: Are there more details to what
exactly might be removed?
I would be in favor of keeping the Raw Gadget driver, at the very
least, as it powers the syzkaller's ability to fuzz USB drivers. The
composite framework is notably less flexible for emulating improper
USB devices (even with the FunctionFS-based composite function).
And as GadgetFS, unlike Raw Gadget right now, supports AIO, it might
make sense to keep it as well.
But I suppose it would be fine to remove the legacy modules that are
just wrappers around various composite functions.
Thank you!
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-05 16:17 ` Alan Stern
2026-08-09 22:19 ` Andrey Konovalov
@ 2026-08-14 16:17 ` Alan Stern
2026-08-17 19:49 ` Minseo Kim
1 sibling, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-08-14 16:17 UTC (permalink / raw)
To: Minseo Kim, Greg Kroah-Hartman
Cc: Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Wed, Aug 05, 2026 at 12:17:22PM -0400, Alan Stern wrote:
> But as you have seen, there are still other problems in that driver.
>
> However, I'm not sure it's worth working on them. Greg KH is talking
> about getting rid of almost all the drivers in the gadget/legacy
> directory. I don't know if that will include the gadgetfs driver. If
> it does, trying to fix up the driver will be a waste of time.
Not having heard anything from Greg, I'll assume that the driver is not
in any imminent danger.
Accordingly, below is a new patch implementing the updates mentioned
last time. In particular, the ep_aio_cancel() carries out its dequeue
operation in a separate thread, and ep_aio() drops dev->lock before
calling kiocb_set_cancel_fn() and usb_ep_queue().
It will probably be necessary to flush the workqueue in
gadgetfs_unbind() after calling destroy_ep_files(), to make sure that
any work routines scheduled for aio requests are no longer running. We
can worry about that later.
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,98 @@ 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;
+ 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();
- return value;
+ usb_ep_dequeue(epdata->ep, priv->req);
+
+ spin_lock_irq(&aio_lock);
+ req_state = priv->req_state;
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irq(&aio_lock);
+
+ /*
+ * priv->req and epdata are freed after unlinking and completion are
+ * both done.
+ * priv itself is freed after unlinking and giveback are both done.
+ */
+ if (req_state >= AIO_COMPLETED) {
+ usb_ep_free_request(epdata->ep, priv->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);
+
+ /*
+ * usb_ep_dequeue() is allowed to run synchronously, calling the
+ * request's completion handler before it returns.
+ * We are called with the aio core holding the aio's context lock,
+ * and ep_aio_complete() below may call iocb->kio_complete(), which
+ * tries to acquire the context lock, which would cause 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 +542,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 +555,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 +570,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 +581,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 +609,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 +625,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 +639,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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-14 16:17 ` Alan Stern
@ 2026-08-17 19:49 ` Minseo Kim
2026-08-18 2:57 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-08-17 19:49 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
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.
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.
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.
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.
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.
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.
Supporting files:
C reproducer:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260814_followup_20260817/repro_candidate_unlink_copy_uaf.c
Build:
gcc -O2 -Wall -Wextra -pthread -o repro_candidate_unlink_copy_uaf \
repro_candidate_unlink_copy_uaf.c
Run:
./repro_candidate_unlink_copy_uaf 1000 32
Diagnostic-only dummy_hcd patch:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260814_followup_20260817/dummy_hcd_crosscpu_giveback_diagnostic.patch
Symbolized KASAN report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260814_followup_20260817/symbolized_report_ep_unlink_worker_uaf.txt
Kernel config used for the unmodified and diagnostic runs:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260814_followup_20260817/kernel.config.kasan_inline_dwarf5_lockdep
I hope this helps.
Best regards,
Minseo Kim
2026년 8월 15일 (토) 오전 1:17, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Wed, Aug 05, 2026 at 12:17:22PM -0400, Alan Stern wrote:
> > But as you have seen, there are still other problems in that driver.
> >
> > However, I'm not sure it's worth working on them. Greg KH is talking
> > about getting rid of almost all the drivers in the gadget/legacy
> > directory. I don't know if that will include the gadgetfs driver. If
> > it does, trying to fix up the driver will be a waste of time.
>
> Not having heard anything from Greg, I'll assume that the driver is not
> in any imminent danger.
>
> Accordingly, below is a new patch implementing the updates mentioned
> last time. In particular, the ep_aio_cancel() carries out its dequeue
> operation in a separate thread, and ep_aio() drops dev->lock before
> calling kiocb_set_cancel_fn() and usb_ep_queue().
>
> It will probably be necessary to flush the workqueue in
> gadgetfs_unbind() after calling destroy_ep_files(), to make sure that
> any work routines scheduled for aio requests are no longer running. We
> can worry about that later.
>
> 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,98 @@ 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;
> + 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();
>
> - return value;
> + usb_ep_dequeue(epdata->ep, priv->req);
> +
> + spin_lock_irq(&aio_lock);
> + req_state = priv->req_state;
> + priv->cancel_state = AIO_UNLINK_DONE;
> + spin_unlock_irq(&aio_lock);
> +
> + /*
> + * priv->req and epdata are freed after unlinking and completion are
> + * both done.
> + * priv itself is freed after unlinking and giveback are both done.
> + */
> + if (req_state >= AIO_COMPLETED) {
> + usb_ep_free_request(epdata->ep, priv->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);
> +
> + /*
> + * usb_ep_dequeue() is allowed to run synchronously, calling the
> + * request's completion handler before it returns.
> + * We are called with the aio core holding the aio's context lock,
> + * and ep_aio_complete() below may call iocb->kio_complete(), which
> + * tries to acquire the context lock, which would cause 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 +542,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 +555,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 +570,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 +581,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 +609,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 +625,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 +639,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:
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-17 19:49 ` Minseo Kim
@ 2026-08-18 2:57 ` Alan Stern
2026-08-20 9:40 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-08-18 2:57 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-18 2:57 ` Alan Stern
@ 2026-08-20 9:40 ` Minseo Kim
2026-08-20 14:08 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-08-20 9:40 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the revised patch and for your kind words about the testing.
I applied it as posted to upstream v7.2-rc1, commit
dc59e4fea9d83f03bad6bddf3fa2e52491777482.
I did not reproduce the previously reported ep_unlink_worker() UAF with
this revision in the same directed cross-CPU diagnostic. I also reran the
original null-ptr-deref and UAF reproducers and the reproducers for the
earlier candidate-patch regressions, and did not observe their
corresponding KASAN signatures.
> Nor any of the old lockdep violations, I trust.
In the matched runs, I did not observe any of the previously reported
LOCKDEP violations or any new violation attributable to this revision.
The only LOCKDEP warning I observed was a ctx_lock IRQ-state warning that
was also reproduced in matched runs on the unpatched kernel.
> What happens if the aio is cancelled exactly between ep_aio()'s calls
> to kiocb_set_cancel_fn() and usb_ep_queue()?
I exercised this exact interval by pausing the submitting thread in a
return probe for kiocb_set_cancel_fn(), before control resumed in ep_aio()
and before usb_ep_queue() was called. I released the submit path either
when the return probe for ep_aio_cancel() ran or, separately, when the
return probe for __x64_sys_io_cancel() ran. Both release points produced
the same results described below.
When I allowed the queue operation to succeed, io_cancel() returned
-EINPROGRESS in both the PWRITE and PREAD cases. ep_aio() then replayed
the cancellation after the queue succeeded, and exactly one completion
event reported res=-ECONNRESET.
When I forced the queue operation to return -EINVAL, io_cancel() again
returned -EINPROGRESS in both cases, and exactly one completion event
reported res=-EINVAL.
I also tested a 64-byte PWRITE for which dummy_hcd completed the request
inside its queue callback. io_cancel() returned -EINPROGRESS, and exactly
one completion event reported res=64.
None of these tested orderings produced an additional completion event,
a KASAN report, or an Oops. In these tested orderings, the AIO_SUBMITTING
handling produced exactly one completion in each case: an early
cancellation was replayed after a pending queue succeeded, a failed queue
produced one completion with its error, and an immediate completion did
not produce a second completion.
I hope this answers the remaining question.
Best regards,
Minseo Kim
2026년 8월 18일 (화) 오전 11:57, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> 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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-20 9:40 ` Minseo Kim
@ 2026-08-20 14:08 ` Alan Stern
2026-08-22 20:26 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-08-20 14:08 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Thu, Aug 20, 2026 at 06:40:19PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the revised patch and for your kind words about the testing.
> I applied it as posted to upstream v7.2-rc1, commit
> dc59e4fea9d83f03bad6bddf3fa2e52491777482.
>
> I did not reproduce the previously reported ep_unlink_worker() UAF with
> this revision in the same directed cross-CPU diagnostic. I also reran the
> original null-ptr-deref and UAF reproducers and the reproducers for the
> earlier candidate-patch regressions, and did not observe their
> corresponding KASAN signatures.
Excellent!
> > Nor any of the old lockdep violations, I trust.
>
> In the matched runs, I did not observe any of the previously reported
> LOCKDEP violations or any new violation attributable to this revision.
> The only LOCKDEP warning I observed was a ctx_lock IRQ-state warning that
> was also reproduced in matched runs on the unpatched kernel.
What was the cause of this warning? If it is sufficiently
straightforward, maybe I can fix it as well.
> > What happens if the aio is cancelled exactly between ep_aio()'s calls
> > to kiocb_set_cancel_fn() and usb_ep_queue()?
>
> I exercised this exact interval by pausing the submitting thread in a
> return probe for kiocb_set_cancel_fn(), before control resumed in ep_aio()
> and before usb_ep_queue() was called. I released the submit path either
> when the return probe for ep_aio_cancel() ran or, separately, when the
> return probe for __x64_sys_io_cancel() ran. Both release points produced
> the same results described below.
>
> When I allowed the queue operation to succeed, io_cancel() returned
> -EINPROGRESS in both the PWRITE and PREAD cases. ep_aio() then replayed
> the cancellation after the queue succeeded, and exactly one completion
> event reported res=-ECONNRESET.
>
> When I forced the queue operation to return -EINVAL, io_cancel() again
> returned -EINPROGRESS in both cases, and exactly one completion event
> reported res=-EINVAL.
>
> I also tested a 64-byte PWRITE for which dummy_hcd completed the request
> inside its queue callback. io_cancel() returned -EINPROGRESS, and exactly
> one completion event reported res=64.
Good, that's exactly what the results should be.
> None of these tested orderings produced an additional completion event,
> a KASAN report, or an Oops. In these tested orderings, the AIO_SUBMITTING
> handling produced exactly one completion in each case: an early
> cancellation was replayed after a pending queue succeeded, a failed queue
> produced one completion with its error, and an immediate completion did
> not produce a second completion.
>
> I hope this answers the remaining question.
Yes, it all sounds good. This patch is just about ready for submission.
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-20 14:08 ` Alan Stern
@ 2026-08-22 20:26 ` Minseo Kim
2026-08-23 1:10 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-08-22 20:26 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you. I am glad the testing has been useful.
> What was the cause of this warning? If it is sufficiently
> straightforward, maybe I can fix it as well.
The warning is caused by ep_aio_cancel() enabling local IRQs while its
caller still holds ctx->ctx_lock. io_cancel() acquires ctx->ctx_lock with
spin_lock_irq() and invokes the cancel callback before releasing that
lock. In the posted revision, ep_aio_cancel() uses spin_lock_irq() for
aio_lock and releases it with spin_unlock_irq(), which enables local IRQs
before the callback returns. LOCKDEP records the resulting SOFTIRQ-ON-W
usage; in the reported run, it later reports inconsistent
softirq-context use of the same lock in the free_ioctx_users() path.
When entered with local IRQs already disabled, the unpatched driver has
the same underlying behavior because ep_aio_cancel() unconditionally
calls local_irq_enable() before returning. Using the same reproducer,
arguments, and kernel configuration, I reproduced the warning on both the
posted revision and the unpatched kernel.
Would it make sense to change the aio_lock operations in
ep_aio_cancel() to spin_lock_irqsave() and spin_unlock_irqrestore(), using
the saved flags on every path that releases the lock? This would preserve
the incoming IRQ state both when the AIO core invokes the callback and
when ep_aio() replays an early cancellation.
I tested this change locally by rerunning, on fresh boots, the full
pre-queue cancellation matrix from my previous message and a longer
cancellation workload of 1000 rounds with 32 requests per round. Each
matrix case produced exactly one expected completion, and neither the
matrix nor the longer workload produced a KASAN report, Oops, or LOCKDEP
warning. If you prefer a different way to preserve the caller's IRQ
state, I would be happy to test that as well.
Separately, my understanding was that we had set the teardown issue aside
for a later discussion. I also tested that case and wanted to share the
result here in case it is useful.
On the posted revision, the reproducer repeatedly triggered:
KASAN: null-ptr-deref in range [0x0000000000000010-0x0000000000000017]
Workqueue: events ep_unlink_worker
RIP: usb_ep_dequeue+0x2c/0x220
drivers/usb/gadget/udc/core.c:333
called from ep_unlink_worker+0x8d/0x1b0
drivers/usb/gadget/legacy/inode.c:477
The endpoint argument to usb_ep_dequeue() was NULL. I also reproduced the
same teardown failure with the local IRQ-state change applied, so the two
issues appear independent.
The reproducer submits AIO FSYNC requests from a thread pinned to CPU 0
and verifies that at least 1024 remain outstanding. The cancel thread is
also pinned to CPU 0, so the work items handled by aio_fsync_work() and
ep_unlink_worker() are both queued through schedule_work() to the CPU 0
worker pool of the system per-CPU workqueue. In the faulting ordering,
after io_cancel() returned -1 with errno set to EINPROGRESS, the main
thread on CPU 1 closed ep0 before ep_unlink_worker() reached
usb_ep_dequeue().
Closing ep0 invoked dev_release(), which called
usb_gadget_unregister_driver(). The unregister path then invoked
gadgetfs_unbind(), where destroy_ep_files() cleared epdata->ep. When
ep_unlink_worker() later reached the dequeue call, it passed the now-NULL
epdata->ep to usb_ep_dequeue().
Quiescing or flushing the relevant unlink work only after
destroy_ep_files() would be too late for this ordering, because
epdata->ep had already been cleared before that synchronization began.
The teardown path therefore appears to need synchronization that prevents
ep_unlink_worker() from dereferencing the endpoint after invalidation,
whether by preventing new unlink_work from being queued and quiescing
pending or running work before invalidation, retaining the endpoint until
such work finishes, or using an equivalent state or lifetime mechanism.
Supporting files:
LOCKDEP report for the posted revision:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260818_lockdep_teardown_followup_20260821/lockdep_ctx_lock_irq_state_warning.txt
Matched unpatched LOCKDEP control report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260818_lockdep_teardown_followup_20260821/lockdep_unpatched_ctx_lock_irq_state_control.txt
C reproducer for the teardown null-ptr-deref:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260818_lockdep_teardown_followup_20260821/repro_unbind_unlink_ep_null.c
Build:
gcc -O2 -Wall -Wextra -pthread -o repro_unbind_unlink_ep_null \
repro_unbind_unlink_ep_null.c
Symbolized KASAN report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260818_lockdep_teardown_followup_20260821/symbolized_report_unbind_unlink_ep_null.txt
Kernel config used for these runs:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_patch_20260818_lockdep_teardown_followup_20260821/kernel.config.kasan_inline_dwarf5_lockdep
I hope this clarifies the warning and provides useful information for
the separate teardown issue.
Best regards,
Minseo Kim
2026년 8월 20일 (목) 오후 11:08, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Thu, Aug 20, 2026 at 06:40:19PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > Thank you for the revised patch and for your kind words about the testing.
> > I applied it as posted to upstream v7.2-rc1, commit
> > dc59e4fea9d83f03bad6bddf3fa2e52491777482.
> >
> > I did not reproduce the previously reported ep_unlink_worker() UAF with
> > this revision in the same directed cross-CPU diagnostic. I also reran the
> > original null-ptr-deref and UAF reproducers and the reproducers for the
> > earlier candidate-patch regressions, and did not observe their
> > corresponding KASAN signatures.
>
> Excellent!
>
> > > Nor any of the old lockdep violations, I trust.
> >
> > In the matched runs, I did not observe any of the previously reported
> > LOCKDEP violations or any new violation attributable to this revision.
> > The only LOCKDEP warning I observed was a ctx_lock IRQ-state warning that
> > was also reproduced in matched runs on the unpatched kernel.
>
> What was the cause of this warning? If it is sufficiently
> straightforward, maybe I can fix it as well.
>
> > > What happens if the aio is cancelled exactly between ep_aio()'s calls
> > > to kiocb_set_cancel_fn() and usb_ep_queue()?
> >
> > I exercised this exact interval by pausing the submitting thread in a
> > return probe for kiocb_set_cancel_fn(), before control resumed in ep_aio()
> > and before usb_ep_queue() was called. I released the submit path either
> > when the return probe for ep_aio_cancel() ran or, separately, when the
> > return probe for __x64_sys_io_cancel() ran. Both release points produced
> > the same results described below.
> >
> > When I allowed the queue operation to succeed, io_cancel() returned
> > -EINPROGRESS in both the PWRITE and PREAD cases. ep_aio() then replayed
> > the cancellation after the queue succeeded, and exactly one completion
> > event reported res=-ECONNRESET.
> >
> > When I forced the queue operation to return -EINVAL, io_cancel() again
> > returned -EINPROGRESS in both cases, and exactly one completion event
> > reported res=-EINVAL.
> >
> > I also tested a 64-byte PWRITE for which dummy_hcd completed the request
> > inside its queue callback. io_cancel() returned -EINPROGRESS, and exactly
> > one completion event reported res=64.
>
> Good, that's exactly what the results should be.
>
> > None of these tested orderings produced an additional completion event,
> > a KASAN report, or an Oops. In these tested orderings, the AIO_SUBMITTING
> > handling produced exactly one completion in each case: an early
> > cancellation was replayed after a pending queue succeeded, a failed queue
> > produced one completion with its error, and an immediate completion did
> > not produce a second completion.
> >
> > I hope this answers the remaining question.
>
> Yes, it all sounds good. This patch is just about ready for submission.
>
> Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-22 20:26 ` Minseo Kim
@ 2026-08-23 1:10 ` Alan Stern
2026-08-28 20:30 ` neck3922
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-08-23 1:10 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Sun, Aug 23, 2026 at 05:26:20AM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you. I am glad the testing has been useful.
>
> > What was the cause of this warning? If it is sufficiently
> > straightforward, maybe I can fix it as well.
>
> The warning is caused by ep_aio_cancel() enabling local IRQs while its
> caller still holds ctx->ctx_lock. io_cancel() acquires ctx->ctx_lock with
> spin_lock_irq() and invokes the cancel callback before releasing that
> lock. In the posted revision, ep_aio_cancel() uses spin_lock_irq() for
> aio_lock and releases it with spin_unlock_irq(), which enables local IRQs
> before the callback returns. LOCKDEP records the resulting SOFTIRQ-ON-W
> usage; in the reported run, it later reports inconsistent
> softirq-context use of the same lock in the free_ioctx_users() path.
>
> When entered with local IRQs already disabled, the unpatched driver has
> the same underlying behavior because ep_aio_cancel() unconditionally
> calls local_irq_enable() before returning. Using the same reproducer,
> arguments, and kernel configuration, I reproduced the warning on both the
> posted revision and the unpatched kernel.
>
> Would it make sense to change the aio_lock operations in
> ep_aio_cancel() to spin_lock_irqsave() and spin_unlock_irqrestore(), using
> the saved flags on every path that releases the lock? This would preserve
> the incoming IRQ state both when the AIO core invokes the callback and
> when ep_aio() replays an early cancellation.
Yes, that is the best solution.
> I tested this change locally by rerunning, on fresh boots, the full
> pre-queue cancellation matrix from my previous message and a longer
> cancellation workload of 1000 rounds with 32 requests per round. Each
> matrix case produced exactly one expected completion, and neither the
> matrix nor the longer workload produced a KASAN report, Oops, or LOCKDEP
> warning. If you prefer a different way to preserve the caller's IRQ
> state, I would be happy to test that as well.
No, that's all good.
> Separately, my understanding was that we had set the teardown issue aside
> for a later discussion. I also tested that case and wanted to share the
> result here in case it is useful.
>
> On the posted revision, the reproducer repeatedly triggered:
>
> KASAN: null-ptr-deref in range [0x0000000000000010-0x0000000000000017]
> Workqueue: events ep_unlink_worker
> RIP: usb_ep_dequeue+0x2c/0x220
> drivers/usb/gadget/udc/core.c:333
> called from ep_unlink_worker+0x8d/0x1b0
> drivers/usb/gadget/legacy/inode.c:477
>
> The endpoint argument to usb_ep_dequeue() was NULL. I also reproduced the
> same teardown failure with the local IRQ-state change applied, so the two
> issues appear independent.
>
> The reproducer submits AIO FSYNC requests from a thread pinned to CPU 0
> and verifies that at least 1024 remain outstanding. The cancel thread is
> also pinned to CPU 0, so the work items handled by aio_fsync_work() and
> ep_unlink_worker() are both queued through schedule_work() to the CPU 0
> worker pool of the system per-CPU workqueue. In the faulting ordering,
> after io_cancel() returned -1 with errno set to EINPROGRESS, the main
> thread on CPU 1 closed ep0 before ep_unlink_worker() reached
> usb_ep_dequeue().
>
> Closing ep0 invoked dev_release(), which called
> usb_gadget_unregister_driver(). The unregister path then invoked
> gadgetfs_unbind(), where destroy_ep_files() cleared epdata->ep. When
> ep_unlink_worker() later reached the dequeue call, it passed the now-NULL
> epdata->ep to usb_ep_dequeue().
>
> Quiescing or flushing the relevant unlink work only after
> destroy_ep_files() would be too late for this ordering, because
> epdata->ep had already been cleared before that synchronization began.
> The teardown path therefore appears to need synchronization that prevents
> ep_unlink_worker() from dereferencing the endpoint after invalidation,
> whether by preventing new unlink_work from being queued and quiescing
> pending or running work before invalidation, retaining the endpoint until
> such work finishes, or using an equivalent state or lifetime mechanism.
destroy_ep_files() clears epdata->ep in order to prevent new I/O
transfers. But this situation involves terminating an existing
transfer, which is quite different. Therefore I think the best approach
is to store a copy of the ep value in priv, so it will be available to
ep_unlink_worker() even after epdata->ep is cleared. (Although the
driver isn't supposed to allocate new requests or start new transfers
after an endpoint is disabled, it is allowed to free existing requests
or try to cancel existing transfers.)
I think it will still be necessary to flush the workqueue after
destroy_ep_files() runs. The best way to check whether this is needed
would be to put a long delay right at the start of ep_unlink_worker()
and then run a test where during that delay, the test program closes its
open files, unmounts the directory, and unloads the gadgetfs module.
A new patch containing the two changes discussed above follows.
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 usb_ep *ep;
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 void ep_unlink_worker(struct work_struct *work)
+{
+ struct kiocb_priv *priv;
+ struct usb_request *req;
+ enum aio_req_state req_state;
+
+ priv = container_of(work, struct kiocb_priv, unlink_work);
+ req = priv->req;
+
+ usb_ep_dequeue(priv->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(priv->ep, req);
+ put_ep(priv->epdata);
+ if (req_state == AIO_GIVEN_BACK)
+ kfree(priv);
+ }
+}
+
static int ep_aio_cancel(struct kiocb *iocb)
{
- struct kiocb_priv *priv = iocb->private;
- struct ep_data *epdata;
- int value;
+ struct kiocb_priv *priv;
+ unsigned long flags;
- local_irq_disable();
- 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();
+ spin_lock_irqsave(&aio_lock, flags);
+ priv = iocb->private;
+ if (!priv || priv->cancel_state != AIO_NOT_CANCELLED) {
+ spin_unlock_irqrestore(&aio_lock, flags);
+ return -EINVAL; /* Already completed or cancelled */
+ }
+ if (priv->req_state == AIO_SUBMITTING) {
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irqrestore(&aio_lock, flags);
+ return 0; /* ep_aio() will call us again if needed */
+ }
- return value;
+ priv->cancel_state = AIO_UNLINKING;
+ spin_unlock_irqrestore(&aio_lock, flags);
+
+ /*
+ * 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,19 +543,25 @@ 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)
{
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,25 +570,35 @@ 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))
- DBG(epdata->dev, "%s fault %d len %d\n",
+ DBG(priv->epdata->dev, "%s fault %d len %d\n",
ep->name, req->status, req->actual);
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(priv->epdata);
+ if (new_req_state == AIO_GIVEN_BACK)
+ kfree(priv);
+ }
}
static ssize_t ep_aio(struct kiocb *iocb,
@@ -532,11 +609,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 +625,12 @@ 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;
+ priv->ep = ep;
- 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:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-23 1:10 ` Alan Stern
@ 2026-08-28 20:30 ` neck3922
2026-08-29 16:08 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: neck3922 @ 2026-08-28 20:30 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the revised patch and for explaining the intended use of
the saved endpoint.
I applied the patch as posted to upstream v7.2-rc1, commit
dc59e4fea9d83f03bad6bddf3fa2e52491777482.
I first tested your revision unchanged. The IRQ-state fix and saved
endpoint behaved as intended. The exact pre-queue cancellation matrix
produced exactly one expected completion in every case, with no KASAN,
Oops, or LOCKDEP output. The original ep_aio_cancel() null-ptr-deref
and UAF signatures, the earlier ep_aio() submit-path regressions, and the
teardown NULL-endpoint failure did not recur in these tests.
> I think it will still be necessary to flush the workqueue after
> destroy_ep_files() runs. The best way to check whether this is needed
> would be to put a long delay right at the start of ep_unlink_worker()
> and then run a test where during that delay, the test program closes its
> open files, unmounts the directory, and unloads the gadgetfs module.
Using CONFIG_USB_GADGETFS=m on the posted revision, I inserted a
diagnostic 15-second delay at the entry of ep_unlink_worker(). While the
worker was delayed, the test process had no open /dev/gadget/* file
descriptors, GadgetFS unmounted successfully, the gadgetfs module
reference count reached zero, and rmmod succeeded before the delay ended.
The delay was diagnostic only.
When the delayed worker resumed, the kernel warned and then panicked. The
serial log showed an unresolved work-function address and identified
gadgetfs as the last unloaded module:
Modules linked in: dummy_hcd [last unloaded: gadgetfs(O)]
Thus, in this path, a running unlink_work item does not pin the gadgetfs
module. Teardown must quiesce pending or running GadgetFS AIO work before
module unload, or otherwise retain the module until that work completes.
While rerunning the directed cross-CPU test from my previous message, I
found that the underlying lifetime race involving ep_unlink_worker()
remained:
BUG: KASAN: slab-use-after-free in ep_unlink_worker
Read of size 8
drivers/usb/gadget/legacy/inode.c:489
The faulting statement was:
put_ep(priv->epdata);
After AIO_UNLINK_DONE was published, completion work could free priv
before the unlink worker's final accesses through priv. Saving epdata and
ep before usb_ep_dequeue() eliminated the failure in the directed test.
Based on our discussion and the results above, I prepared the cumulative
patch below, incorporating your latest revision. It also fixes an
existing native PREAD issue: the to_free predicate could skip copying an
ITER_UBUF payload while still reporting success.
During development and validation, I found two further ordering issues.
In the existing completion ordering, request cleanup could overlap
usb_ep_disable() during final endpoint release. Separately, an
intermediate cumulative version allowed gadgetfs_unbind() to pass the
completion-work flush before a callback had queued that work. The
resulting patch addresses all three issues and applies directly to
upstream v7.2-rc1.
I reran the exact pre-queue matrix and earlier regression reproducers,
together with native PREAD/PREADV/PWRITE/PWRITEV payload checks and
directed cross-CPU and deferred-giveback tests. I also exercised teardown
and endpoint-release orderings, module lifecycle, CPU hotplug, rebind,
io_destroy, process exit, and partial-read cancellation stress. Across
the tested GadgetFS AIO lifetime and teardown paths, I observed no KASAN
report, Oops, LOCKDEP warning, stall, duplicate completion, or userspace
failure.
Targeted DEBUG_OBJECTS runs were clean, and strict KCSAN reported no race
involving GadgetFS or these AIO work functions. The incremental changes
passed strict checkpatch; the resulting source passed a W=1 module build,
a built-in CONFIG_USB_GADGETFS=y build, and a C=2 W=1 sparse check.
Supporting files for the two diagnostics above:
Cross-CPU reproducer:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_20260825_support/repro_candidate_unlink_copy_uaf.c
Diagnostic-only dummy_hcd patch:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_20260825_support/candidate_20260814_diag_crosscpu_work_on_cpu.patch
Symbolized ep_unlink_worker() KASAN report:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_20260825_support/symbolized_report_ep_unlink_worker_uaf.txt
Worker-entry delay diagnostic and module-unload result:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_20260825_support/candidate_20260822_diag_unlink_entry_delay.patch
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_20260825_support/teardown_module_unload_serial.log
Kernel config used for these diagnostics:
https://raw.githubusercontent.com/neck392/linux-kernel-bug-reports/main/gadgetfs_candidate_20260825_support/kernel.config.kasan_inline_dwarf5_lockdep
If I have misunderstood any part of the intended lifetime or teardown
behavior, please let me know. I would be glad to run any additional tests
if needed. :-)
With sincere appreciation and respect,
Minseo Kim
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
@@ -29,6 +29,7 @@
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/moduleparam.h>
+#include <linux/workqueue.h>
#include <linux/usb/gadgetfs.h>
#include <linux/usb/gadget.h>
@@ -123,6 +124,8 @@
spinlock_t lock;
refcount_t count;
int udc_usage;
+ int aio_producers; /* P: lock */
+ bool aio_shutdown; /* P: aio_lock */
enum ep0_state state; /* P: lock */
struct usb_gadgetfs_event event [N_EVENT];
unsigned ev_next;
@@ -236,6 +239,10 @@
static const char *CHIP;
static DEFINE_MUTEX(sb_mutex); /* Serialize superblock operations */
+static DEFINE_SPINLOCK(aio_lock); /* Protect GadgetFS AIO state */
+static struct workqueue_struct *gadgetfs_unlink_wq;
+static struct workqueue_struct *gadgetfs_copy_wq;
+
/*----------------------------------------------------------------------*/
/* NOTE: don't use dev_printk calls before binding to the gadget
@@ -392,6 +399,8 @@
data->state = STATE_EP_DISABLED;
data->desc.bDescriptorType = 0;
data->hs_desc.bDescriptorType = 0;
+ /* Wait for unlink work before disabling the endpoint. */
+ flush_workqueue(gadgetfs_unlink_wq);
usb_ep_disable(data->ep);
}
mutex_unlock(&data->lock);
@@ -433,95 +442,189 @@
/* 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 usb_ep *ep;
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;
+ int status;
+ 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_ep *ep;
+ 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();
+ ep = priv->ep;
+ req = priv->req;
- return value;
+ usb_ep_dequeue(ep, req);
+
+ spin_lock_irq(&aio_lock);
+ req_state = priv->req_state;
+ priv->cancel_state = AIO_UNLINK_DONE;
+ if (req_state >= AIO_COMPLETED)
+ priv->req = NULL;
+ spin_unlock_irq(&aio_lock);
+
+ /*
+ * If the callback has already published AIO_COMPLETED, this worker
+ * claims request cleanup. Free priv only after copy_work publishes
+ * AIO_GIVEN_BACK.
+ */
+ if (req_state >= AIO_COMPLETED) {
+ usb_ep_free_request(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;
+ unsigned long flags;
+
+ spin_lock_irqsave(&aio_lock, flags);
+ priv = iocb->private;
+ if (!priv || priv->cancel_state != AIO_NOT_CANCELLED) {
+ spin_unlock_irqrestore(&aio_lock, flags);
+ return -EINVAL; /* No longer cancellable */
+ }
+ if (priv->req_state == AIO_SUBMITTING) {
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irqrestore(&aio_lock, flags);
+ return 0; /* ep_aio() will call us again if needed */
+ }
+ if (priv->epdata->dev->aio_shutdown) {
+ spin_unlock_irqrestore(&aio_lock, flags);
+ return -EINVAL; /* Leave completion to the giveback path */
+ }
+
+ /*
+ * The AIO core may call us with the iocb context lock held. Run the
+ * dequeue in work context, and complete the iocb from copy_work, so a
+ * synchronous giveback cannot re-enter the AIO core from this callback.
+ *
+ * Queue under aio_lock so unbind cannot set aio_shutdown and flush
+ * the workqueue before this work is visible.
+ */
+ priv->cancel_state = AIO_UNLINKING;
+ queue_work(gadgetfs_unlink_wq, &priv->unlink_work);
+ spin_unlock_irqrestore(&aio_lock, flags);
+ 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 usb_request *req = NULL;
+ struct ep_data *epdata = NULL;
+ struct usb_ep *ep = NULL;
struct mm_struct *mm = priv->mm;
struct kiocb *iocb = priv->iocb;
- size_t ret;
+ ssize_t ret;
- kthread_use_mm(mm);
- ret = copy_to_iter(priv->buf, priv->actual, &priv->to);
- kthread_unuse_mm(mm);
- if (!ret)
- ret = -EFAULT;
+ /* Writes leave priv->to empty; reads retain the destination iterator. */
+ if (!iov_iter_count(&priv->to) || !priv->actual) {
+ ret = priv->actual ? (ssize_t)priv->actual :
+ (ssize_t)priv->status;
+ } else {
+ kthread_use_mm(mm);
+ ret = copy_to_iter(priv->buf, priv->actual, &priv->to);
+ kthread_unuse_mm(mm);
+ if (!ret)
+ ret = -EFAULT;
+ }
+
+ /* Claim request cleanup unless the unlink worker owns it. */
+ spin_lock_irq(&aio_lock);
+ if (priv->cancel_state != AIO_UNLINKING && priv->req) {
+ req = priv->req;
+ epdata = priv->epdata;
+ ep = priv->ep;
+ priv->req = NULL;
+ }
+ spin_unlock_irq(&aio_lock);
+
+ if (req) {
+ mutex_lock(&epdata->lock);
+ usb_ep_free_request(ep, req);
+ mutex_unlock(&epdata->lock);
+ put_ep(epdata);
+ }
- /* completing the iocb can drop the ctx and mm, don't touch mm after */
+ /* Completing the iocb can drop the file, ctx, and mm. */
iocb->ki_complete(iocb, ret);
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)
{
struct kiocb *iocb = req->context;
struct kiocb_priv *priv = iocb->private;
- struct ep_data *epdata = priv->epdata;
+ struct dev_data *dev = priv->epdata->dev;
- /* lock against disconnect (and ideally, cancel) */
- spin_lock(&epdata->dev->lock);
- priv->req = NULL;
- priv->epdata = NULL;
-
- /* if this was a write or a read returning no data then we
- * don't need to copy anything to userspace, so we can
- * complete the aio request immediately.
- */
- 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);
- } else {
- /* 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",
- ep->name, req->status, req->actual);
-
- priv->buf = req->buf;
- priv->actual = req->actual;
- INIT_WORK(&priv->work, ep_user_copy_worker);
- schedule_work(&priv->work);
- }
-
- usb_ep_free_request(ep, req);
- spin_unlock(&epdata->dev->lock);
- put_ep(epdata);
+ /* Prevent future cancellation */
+ spin_lock(&aio_lock);
+ iocb->private = NULL;
+ spin_unlock(&aio_lock);
+
+ /* Log status errors hidden by a nonzero actual count. */
+ if (iov_iter_count(&priv->to) && req->actual &&
+ unlikely(req->status != 0))
+ DBG(priv->epdata->dev, "%s fault %d len %d\n",
+ ep->name, req->status, req->actual);
+
+ priv->buf = req->buf;
+ priv->actual = req->actual;
+ priv->status = req->status;
+
+ spin_lock(&aio_lock);
+ priv->req_state = AIO_COMPLETED;
+ spin_unlock(&aio_lock);
+
+ INIT_WORK(&priv->copy_work, ep_user_copy_worker);
+ /* Publish copy work before unbind can observe the last producer. */
+ spin_lock(&dev->lock);
+ queue_work(gadgetfs_copy_wq, &priv->copy_work);
+ --dev->aio_producers;
+ spin_unlock(&dev->lock);
}
static ssize_t ep_aio(struct kiocb *iocb,
@@ -532,11 +635,13 @@
{
struct usb_request *req;
ssize_t value;
+ struct usb_ep *ep;
+ bool need_unlink = false;
+ INIT_WORK(&priv->unlink_work, ep_unlink_worker);
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 +652,12 @@
*/
spin_lock_irq(&epdata->dev->lock);
value = -ENODEV;
- if (unlikely(epdata->ep == NULL))
+ ep = epdata->ep;
+ if (unlikely(ep == NULL))
goto fail;
+ priv->ep = ep;
- 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 +667,48 @@
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;
+ /* Account for the callback before the request is visible to the UDC. */
+ ++epdata->dev->aio_producers;
+ 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);
+ --epdata->dev->aio_producers;
+ 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:
@@ -1640,8 +1783,12 @@
gadgetfs_unbind (struct usb_gadget *gadget)
{
struct dev_data *dev = get_gadget_data (gadget);
+ unsigned long flags;
DBG (dev, "%s\n", __func__);
+ spin_lock_irqsave(&aio_lock, flags);
+ dev->aio_shutdown = true;
+ spin_unlock_irqrestore(&aio_lock, flags);
spin_lock_irq (&dev->lock);
dev->state = STATE_DEV_UNBOUND;
@@ -1652,7 +1799,21 @@
}
spin_unlock_irq (&dev->lock);
+ /* Wait for unlink work before disabling endpoints. */
+ flush_workqueue(gadgetfs_unlink_wq);
destroy_ep_files (dev);
+
+ /* Wait until completion callbacks have published their cleanup work. */
+ spin_lock_irq(&dev->lock);
+ while (dev->aio_producers > 0) {
+ spin_unlock_irq(&dev->lock);
+ usleep_range(1000, 2000);
+ spin_lock_irq(&dev->lock);
+ }
+ spin_unlock_irq(&dev->lock);
+
+ /* Wait for completion work queued by the callbacks. */
+ flush_workqueue(gadgetfs_copy_wq);
gadget->ep0->driver_data = NULL;
set_gadget_data (gadget, NULL);
@@ -1677,6 +1838,9 @@
shortname, CHIP, gadget->name);
return -ENODEV;
}
+ spin_lock_irq(&aio_lock);
+ dev->aio_shutdown = false;
+ spin_unlock_irq(&aio_lock);
set_gadget_data (gadget, dev);
dev->gadget = gadget;
@@ -2125,10 +2289,25 @@
{
int status;
+ gadgetfs_unlink_wq = alloc_workqueue("gadgetfs_unlink", WQ_PERCPU, 0);
+ if (!gadgetfs_unlink_wq)
+ return -ENOMEM;
+
+ gadgetfs_copy_wq = alloc_workqueue("gadgetfs_copy", WQ_PERCPU, 0);
+ if (!gadgetfs_copy_wq) {
+ destroy_workqueue(gadgetfs_unlink_wq);
+ return -ENOMEM;
+ }
+
status = register_filesystem (&gadgetfs_type);
- if (status == 0)
- pr_info ("%s: %s, version " DRIVER_VERSION "\n",
- shortname, driver_desc);
+ if (status) {
+ destroy_workqueue(gadgetfs_copy_wq);
+ destroy_workqueue(gadgetfs_unlink_wq);
+ return status;
+ }
+
+ pr_info("%s: %s, version " DRIVER_VERSION "\n",
+ shortname, driver_desc);
return status;
}
module_init (gadgetfs_init);
@@ -2137,6 +2316,7 @@
{
pr_debug ("unregister %s\n", shortname);
unregister_filesystem (&gadgetfs_type);
+ destroy_workqueue(gadgetfs_unlink_wq);
+ destroy_workqueue(gadgetfs_copy_wq);
}
module_exit (gadgetfs_cleanup);
-
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-28 20:30 ` neck3922
@ 2026-08-29 16:08 ` Alan Stern
2026-08-31 13:30 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-08-29 16:08 UTC (permalink / raw)
To: neck3922
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Fri, Aug 28, 2026 at 03:30:10PM -0500, neck3922@gmail.com wrote:
> Hi Alan,
>
> Thank you for the revised patch and for explaining the intended use of
> the saved endpoint.
>
> I applied the patch as posted to upstream v7.2-rc1, commit
> dc59e4fea9d83f03bad6bddf3fa2e52491777482.
>
> I first tested your revision unchanged. The IRQ-state fix and saved
> endpoint behaved as intended. The exact pre-queue cancellation matrix
> produced exactly one expected completion in every case, with no KASAN,
> Oops, or LOCKDEP output. The original ep_aio_cancel() null-ptr-deref
> and UAF signatures, the earlier ep_aio() submit-path regressions, and the
> teardown NULL-endpoint failure did not recur in these tests.
All good results.
> > I think it will still be necessary to flush the workqueue after
> > destroy_ep_files() runs. The best way to check whether this is needed
> > would be to put a long delay right at the start of ep_unlink_worker()
> > and then run a test where during that delay, the test program closes its
> > open files, unmounts the directory, and unloads the gadgetfs module.
>
> Using CONFIG_USB_GADGETFS=m on the posted revision, I inserted a
> diagnostic 15-second delay at the entry of ep_unlink_worker(). While the
> worker was delayed, the test process had no open /dev/gadget/* file
> descriptors, GadgetFS unmounted successfully, the gadgetfs module
> reference count reached zero, and rmmod succeeded before the delay ended.
> The delay was diagnostic only.
>
> When the delayed worker resumed, the kernel warned and then panicked. The
> serial log showed an unresolved work-function address and identified
> gadgetfs as the last unloaded module:
>
> Modules linked in: dummy_hcd [last unloaded: gadgetfs(O)]
>
> Thus, in this path, a running unlink_work item does not pin the gadgetfs
> module. Teardown must quiesce pending or running GadgetFS AIO work before
> module unload, or otherwise retain the module until that work completes.
Indeed. Quiescing won't be easy to do; it will just have to wait until
the work is complete. In other words, flush the workqueue.
> While rerunning the directed cross-CPU test from my previous message, I
> found that the underlying lifetime race involving ep_unlink_worker()
> remained:
>
> BUG: KASAN: slab-use-after-free in ep_unlink_worker
> Read of size 8
> drivers/usb/gadget/legacy/inode.c:489
>
> The faulting statement was:
>
> put_ep(priv->epdata);
>
> After AIO_UNLINK_DONE was published, completion work could free priv
> before the unlink worker's final accesses through priv. Saving epdata and
> ep before usb_ep_dequeue() eliminated the failure in the directed test.
Ah, yes. I should have realized that last time. I just didn't think
hard enough about how the ordering could interact with those tests at
the end of ep_unlink_worker(). (Or maybe I did and then forgot to
include the changes into the patch -- I can't remember.) Anyway, my
version of the patch has been updated accordingly.
> Based on our discussion and the results above, I prepared the cumulative
> patch below, incorporating your latest revision. It also fixes an
> existing native PREAD issue: the to_free predicate could skip copying an
> ITER_UBUF payload while still reporting success.
I'm not sure what you're referring to. Are you saying that this test
in ep_read_iter():
if (!iter_is_ubuf(&priv->to) && !priv->to_free) {
is wrong, for example, the && should be || ? In fact, I don't
understand the reason for the iter_is_ubuf() check at all.
Note that to_free isn't a predicate; rather it's a pointer to a copy of
an iov_iter structure.
> During development and validation, I found two further ordering issues.
> In the existing completion ordering, request cleanup could overlap
> usb_ep_disable() during final endpoint release.
Yes, I know. I understood that it was okay to call
usb_ep_free_request() after usb_ep_disable(). Was that wrong? Did you
see it create any problems?
Note that usb_ep_disable() doesn't return until all the requests queued
for that endpoint have completed. So ep_aio_complete() will have run,
but the work routines may still be pending.
> Separately, an
> intermediate cumulative version allowed gadgetfs_unbind() to pass the
> completion-work flush before a callback had queued that work.
This is probably because you were flushing the workqueues at the wrong
time.
For the final submission, I think the workqueue management stuff should
go into its own separate patch. Straightening out the various AIO races
is already complicated enough by itself.
> The
> resulting patch addresses all three issues and applies directly to
> upstream v7.2-rc1.
I'll review the patch later. For now, there's two things to mention.
First, when you create your patches, it would help to add the -p
option to the diff command.
Second, why did you change ep_aio_complete() to make it queue up
ep_user_copy_worker() even when nothing needed to be copied to
userspace? It's a bad idea to run a workqueue routine if it isn't
necessary.
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-29 16:08 ` Alan Stern
@ 2026-08-31 13:30 ` Minseo Kim
2026-09-01 2:47 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-08-31 13:30 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the comments and for pointing out where my explanation was
unclear.
> Are you saying that this test in ep_read_iter():
>
> if (!iter_is_ubuf(&priv->to) && !priv->to_free) {
>
> is wrong, for example, the && should be || ?
No. The && condition should remain as it is. dup_iter() copies the iterator
state into priv->to. In my tests, native PREAD and a one-segment PREADV used
ITER_UBUF. Since ITER_UBUF has no separate iovec array to duplicate,
dup_iter() returned NULL while the copied iterator remained valid. My
two-segment PREADV test used ITER_IOVEC; there, a NULL return would mean that
allocation of the duplicated iovec array failed. The iter_is_ubuf() check
therefore distinguishes the valid NULL return for ITER_UBUF from an allocation
failure in the ITER_IOVEC case. In a control build using ||, valid PREAD and
both one- and two-segment PREADV submissions failed with -ENOMEM.
The issue I observed was instead in the later completion test:
if (priv->to_free == NULL || unlikely(req->actual == 0)) {
As I understand it, this later test treats a NULL to_free pointer as meaning
that no userspace copy is needed. In this path, however, to_free stores the
pointer returned by dup_iter() and tracks separately allocated iterator
backing, rather than whether the saved iterator has a destination.
For this issue, the relevant change was to base the later copy decision on
iov_iter_count(&priv->to); I did not change the ep_read_iter() condition. With
the original test, native PREAD reported res=511 while the destination buffer
remained unchanged. After this change, native PREAD and one-segment PREADV
copied the payload correctly, and two-segment PREADV continued to work.
My previous description of to_free as a predicate was inaccurate. I was
referring to its later completion-side use as a copy/no-copy indicator.
> I understood that it was okay to call usb_ep_free_request() after
> usb_ep_disable(). Was that wrong? Did you see it create any problems?
Your understanding matches the behavior I observed under dummy_hcd. For both
PWRITE and PREAD, I kept a completed, unqueued request allocated until after
usb_ep_disable() returned, with no intervening usb_ep_dequeue(). In each case,
the request completed exactly once with the expected result; the subsequent
usb_ep_free_request() produced no KASAN report or Oops.
Separately, in an intermediate version I observed usb_ep_free_request()
running while usb_ep_disable() was still in progress. Because that version
moved normal request cleanup into a worker, I used the existing endpoint mutex
to prevent the worker's usb_ep_free_request() call from overlapping endpoint
disable while I investigated the ordering. The mutex eliminated the overlap,
but I did not reproduce a failure without it, so these tests did not establish
that the serialization was required.
The endpoint-use ordering I could reproduce in a directed dummy_hcd diagnostic
was different: without waiting for unlink work before endpoint disable,
ep_unlink_worker() could call usb_ep_dequeue() while usb_ep_disable() was in
progress. In matched PWRITE and PREAD tests, the variant without the
pre-disable unlink-work wait produced this overlap; with the wait retained,
usb_ep_dequeue() completed before usb_ep_disable() began.
I did not reproduce a KASAN report, Oops, or userspace failure from the
concurrent dequeue/disable overlap itself. I retained the pre-disable wait in
the narrower test version as a conservative interpretation of the
usb_ep_disable() requirement that no other task be using the endpoint when it
is called.
> This is probably because you were flushing the workqueues at the wrong
> time.
Yes. In the callback-gate test, the relevant workqueue flush returned
while the completion callback was stopped before queueing its follow-up work.
In the narrower test version, I incremented aio_producers before
usb_ep_queue() and decremented it only after the callback had either queued
the required follow-up work or completed the immediate AIO result without
leaving deferred work to publish. If usb_ep_queue() failed, the submission
path decremented it directly. With that change, gadgetfs_unbind() remained
blocked in the producer wait while the callback was gated. It proceeded to
flush the completion workqueue only after the callback queued the follow-up
work.
In the delayed-worker test, module unload completed before the running
unlink_work returned. This confirmed that module unload must not complete
while such work is still running.
> For the final submission, I think the workqueue management stuff should
> go into its own separate patch.
Separating the workqueue management changes from the AIO race fixes makes
sense. Thank you also for the reminder about -p.
> Second, why did you change ep_aio_complete() to make it queue up
> ep_user_copy_worker() even when nothing needed to be copied to
> userspace?
My reason for routing every completion through ep_user_copy_worker() was to
make a common deferred completion and request-cleanup stage visible to a
workqueue flush during teardown, not because every request needed a userspace
copy. In the normal non-unlink path of that cumulative design, the worker
released the USB request and epdata reference before calling ki_complete(),
and then published AIO_GIVEN_BACK. Since ki_complete() could drop the last AIO
file reference and allow ep_release() to begin, the intent was to prevent
teardown from passing the completion-work flush before that cleanup had
finished. A gate placed between cleanup and ki_complete() confirmed that the
flush covered this no-copy PWRITE cleanup: unbind reached
flush_workqueue(gadgetfs_copy_wq) but did not return until the worker was
released.
I agree that using the worker for every completion was broader than necessary
for copy handling. The narrower behavior I tested retains aio_producers but
queues copy work only when iov_iter_count(&priv->to) and req->actual are both
nonzero. Nonzero PREAD and the one- and two-segment PREADV cases invoked the
copy worker, while PWRITE, PWRITEV, and zero-length reads and writes did not.
This indicates that no-copy paths do not need ep_user_copy_worker() merely for
copy handling. Their cleanup ordering and the separate teardown lifetime
issue can be considered independently of that routing decision.
Looking back, trying to address all of the observed issues in one cumulative
patch made it harder to separate and review the purpose of each change.
Considering the issues independently may also make it easier to identify a
simpler approach for some of them. I hope these clarifications and test
results are useful.
Thank you again for your comments.
Best regards,
Minseo Kim
2026년 8월 30일 (일) 오전 1:08, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Fri, Aug 28, 2026 at 03:30:10PM -0500, neck3922@gmail.com wrote:
> > Hi Alan,
> >
> > Thank you for the revised patch and for explaining the intended use of
> > the saved endpoint.
> >
> > I applied the patch as posted to upstream v7.2-rc1, commit
> > dc59e4fea9d83f03bad6bddf3fa2e52491777482.
> >
> > I first tested your revision unchanged. The IRQ-state fix and saved
> > endpoint behaved as intended. The exact pre-queue cancellation matrix
> > produced exactly one expected completion in every case, with no KASAN,
> > Oops, or LOCKDEP output. The original ep_aio_cancel() null-ptr-deref
> > and UAF signatures, the earlier ep_aio() submit-path regressions, and the
> > teardown NULL-endpoint failure did not recur in these tests.
>
> All good results.
>
> > > I think it will still be necessary to flush the workqueue after
> > > destroy_ep_files() runs. The best way to check whether this is needed
> > > would be to put a long delay right at the start of ep_unlink_worker()
> > > and then run a test where during that delay, the test program closes its
> > > open files, unmounts the directory, and unloads the gadgetfs module.
> >
> > Using CONFIG_USB_GADGETFS=m on the posted revision, I inserted a
> > diagnostic 15-second delay at the entry of ep_unlink_worker(). While the
> > worker was delayed, the test process had no open /dev/gadget/* file
> > descriptors, GadgetFS unmounted successfully, the gadgetfs module
> > reference count reached zero, and rmmod succeeded before the delay ended.
> > The delay was diagnostic only.
> >
> > When the delayed worker resumed, the kernel warned and then panicked. The
> > serial log showed an unresolved work-function address and identified
> > gadgetfs as the last unloaded module:
> >
> > Modules linked in: dummy_hcd [last unloaded: gadgetfs(O)]
> >
> > Thus, in this path, a running unlink_work item does not pin the gadgetfs
> > module. Teardown must quiesce pending or running GadgetFS AIO work before
> > module unload, or otherwise retain the module until that work completes.
>
> Indeed. Quiescing won't be easy to do; it will just have to wait until
> the work is complete. In other words, flush the workqueue.
>
> > While rerunning the directed cross-CPU test from my previous message, I
> > found that the underlying lifetime race involving ep_unlink_worker()
> > remained:
> >
> > BUG: KASAN: slab-use-after-free in ep_unlink_worker
> > Read of size 8
> > drivers/usb/gadget/legacy/inode.c:489
> >
> > The faulting statement was:
> >
> > put_ep(priv->epdata);
> >
> > After AIO_UNLINK_DONE was published, completion work could free priv
> > before the unlink worker's final accesses through priv. Saving epdata and
> > ep before usb_ep_dequeue() eliminated the failure in the directed test.
>
> Ah, yes. I should have realized that last time. I just didn't think
> hard enough about how the ordering could interact with those tests at
> the end of ep_unlink_worker(). (Or maybe I did and then forgot to
> include the changes into the patch -- I can't remember.) Anyway, my
> version of the patch has been updated accordingly.
>
> > Based on our discussion and the results above, I prepared the cumulative
> > patch below, incorporating your latest revision. It also fixes an
> > existing native PREAD issue: the to_free predicate could skip copying an
> > ITER_UBUF payload while still reporting success.
>
> I'm not sure what you're referring to. Are you saying that this test
> in ep_read_iter():
>
> if (!iter_is_ubuf(&priv->to) && !priv->to_free) {
>
> is wrong, for example, the && should be || ? In fact, I don't
> understand the reason for the iter_is_ubuf() check at all.
>
> Note that to_free isn't a predicate; rather it's a pointer to a copy of
> an iov_iter structure.
>
> > During development and validation, I found two further ordering issues.
> > In the existing completion ordering, request cleanup could overlap
> > usb_ep_disable() during final endpoint release.
>
> Yes, I know. I understood that it was okay to call
> usb_ep_free_request() after usb_ep_disable(). Was that wrong? Did you
> see it create any problems?
>
> Note that usb_ep_disable() doesn't return until all the requests queued
> for that endpoint have completed. So ep_aio_complete() will have run,
> but the work routines may still be pending.
>
> > Separately, an
> > intermediate cumulative version allowed gadgetfs_unbind() to pass the
> > completion-work flush before a callback had queued that work.
>
> This is probably because you were flushing the workqueues at the wrong
> time.
>
> For the final submission, I think the workqueue management stuff should
> go into its own separate patch. Straightening out the various AIO races
> is already complicated enough by itself.
>
> > The
> > resulting patch addresses all three issues and applies directly to
> > upstream v7.2-rc1.
>
> I'll review the patch later. For now, there's two things to mention.
> First, when you create your patches, it would help to add the -p
> option to the diff command.
>
> Second, why did you change ep_aio_complete() to make it queue up
> ep_user_copy_worker() even when nothing needed to be copied to
> userspace? It's a bad idea to run a workqueue routine if it isn't
> necessary.
>
> Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-08-31 13:30 ` Minseo Kim
@ 2026-09-01 2:47 ` Alan Stern
2026-09-04 14:00 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-01 2:47 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Mon, Aug 31, 2026 at 10:30:08PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the comments and for pointing out where my explanation was
> unclear.
>
> > Are you saying that this test in ep_read_iter():
> >
> > if (!iter_is_ubuf(&priv->to) && !priv->to_free) {
> >
> > is wrong, for example, the && should be || ?
>
> No. The && condition should remain as it is. dup_iter() copies the iterator
> state into priv->to. In my tests, native PREAD and a one-segment PREADV used
> ITER_UBUF. Since ITER_UBUF has no separate iovec array to duplicate,
> dup_iter() returned NULL while the copied iterator remained valid. My
> two-segment PREADV test used ITER_IOVEC; there, a NULL return would mean that
> allocation of the duplicated iovec array failed. The iter_is_ubuf() check
> therefore distinguishes the valid NULL return for ITER_UBUF from an allocation
> failure in the ITER_IOVEC case. In a control build using ||, valid PREAD and
> both one- and two-segment PREADV submissions failed with -ENOMEM.
>
> The issue I observed was instead in the later completion test:
>
> if (priv->to_free == NULL || unlikely(req->actual == 0)) {
>
> As I understand it, this later test treats a NULL to_free pointer as meaning
> that no userspace copy is needed. In this path, however, to_free stores the
> pointer returned by dup_iter() and tracks separately allocated iterator
> backing, rather than whether the saved iterator has a destination.
Okay, I see. I had assumed that the information about the userspace
buffer was contained in the region that to_free points to.
> For this issue, the relevant change was to base the later copy decision on
> iov_iter_count(&priv->to); I did not change the ep_read_iter() condition. With
> the original test, native PREAD reported res=511 while the destination buffer
> remained unchanged. After this change, native PREAD and one-segment PREADV
> copied the payload correctly, and two-segment PREADV continued to work.
While that's a valid solution, it would be more straightforward to add
an "is_read" flag to the priv structure. If an aio operation isn't a
read then there certainly won't be anything to copy back to userspace
upon completion. This doesn't require people reading the source to know
anything about the internal details of struct iov_iter. And it is a
better match to the immediately preceding comment.
> My previous description of to_free as a predicate was inaccurate. I was
> referring to its later completion-side use as a copy/no-copy indicator.
>
> > I understood that it was okay to call usb_ep_free_request() after
> > usb_ep_disable(). Was that wrong? Did you see it create any problems?
>
> Your understanding matches the behavior I observed under dummy_hcd. For both
> PWRITE and PREAD, I kept a completed, unqueued request allocated until after
> usb_ep_disable() returned, with no intervening usb_ep_dequeue(). In each case,
> the request completed exactly once with the expected result; the subsequent
> usb_ep_free_request() produced no KASAN report or Oops.
>
> Separately, in an intermediate version I observed usb_ep_free_request()
> running while usb_ep_disable() was still in progress. Because that version
> moved normal request cleanup into a worker, I used the existing endpoint mutex
> to prevent the worker's usb_ep_free_request() call from overlapping endpoint
> disable while I investigated the ordering. The mutex eliminated the overlap,
> but I did not reproduce a failure without it, so these tests did not establish
> that the serialization was required.
Good, so we don't need to worry about that.
> The endpoint-use ordering I could reproduce in a directed dummy_hcd diagnostic
> was different: without waiting for unlink work before endpoint disable,
> ep_unlink_worker() could call usb_ep_dequeue() while usb_ep_disable() was in
> progress. In matched PWRITE and PREAD tests, the variant without the
> pre-disable unlink-work wait produced this overlap; with the wait retained,
> usb_ep_dequeue() completed before usb_ep_disable() began.
>
> I did not reproduce a KASAN report, Oops, or userspace failure from the
> concurrent dequeue/disable overlap itself. I retained the pre-disable wait in
> the narrower test version as a conservative interpretation of the
> usb_ep_disable() requirement that no other task be using the endpoint when it
> is called.
We don't need to be that conservative here. And in fact, I'm not
entirely sure what that comment in usb_ep_disable() was intended to
mean. Since the function doesn't cause the UDC driver's endpoint data
structure to be deallocated, there isn't much to be careful of.
> > This is probably because you were flushing the workqueues at the wrong
> > time.
>
> Yes. In the callback-gate test, the relevant workqueue flush returned
> while the completion callback was stopped before queueing its follow-up work.
> In the narrower test version, I incremented aio_producers before
> usb_ep_queue() and decremented it only after the callback had either queued
> the required follow-up work or completed the immediate AIO result without
> leaving deferred work to publish. If usb_ep_queue() failed, the submission
> path decremented it directly. With that change, gadgetfs_unbind() remained
> blocked in the producer wait while the callback was gated. It proceeded to
> flush the completion workqueue only after the callback queued the follow-up
> work.
>
> In the delayed-worker test, module unload completed before the running
> unlink_work returned. This confirmed that module unload must not complete
> while such work is still running.
>
> > For the final submission, I think the workqueue management stuff should
> > go into its own separate patch.
>
> Separating the workqueue management changes from the AIO race fixes makes
> sense. Thank you also for the reminder about -p.
So let's worry about the workqueue stuff later and concentrate for now
just on fixing the AIO races.
> > Second, why did you change ep_aio_complete() to make it queue up
> > ep_user_copy_worker() even when nothing needed to be copied to
> > userspace?
>
> My reason for routing every completion through ep_user_copy_worker() was to
> make a common deferred completion and request-cleanup stage visible to a
> workqueue flush during teardown, not because every request needed a userspace
> copy. In the normal non-unlink path of that cumulative design, the worker
> released the USB request and epdata reference before calling ki_complete(),
> and then published AIO_GIVEN_BACK. Since ki_complete() could drop the last AIO
> file reference and allow ep_release() to begin, the intent was to prevent
> teardown from passing the completion-work flush before that cleanup had
> finished. A gate placed between cleanup and ki_complete() confirmed that the
> flush covered this no-copy PWRITE cleanup: unbind reached
> flush_workqueue(gadgetfs_copy_wq) but did not return until the worker was
> released.
>
> I agree that using the worker for every completion was broader than necessary
> for copy handling. The narrower behavior I tested retains aio_producers but
> queues copy work only when iov_iter_count(&priv->to) and req->actual are both
> nonzero. Nonzero PREAD and the one- and two-segment PREADV cases invoked the
> copy worker, while PWRITE, PWRITEV, and zero-length reads and writes did not.
> This indicates that no-copy paths do not need ep_user_copy_worker() merely for
> copy handling. Their cleanup ordering and the separate teardown lifetime
> issue can be considered independently of that routing decision.
And since your decision was based on handling the workqueue flushing, it
can be put off until later.
If you trim your patch down to the parts that only target the AIO races,
do you get anything significantly different from my version? I did see
that you added or changed a few comments, and I have updated my version
of the patch in a couple of respects, but overall yours must end up
being pretty similar to mine. Please point out any differences you
believe are worth mentioning. My current version is below.
> Looking back, trying to address all of the observed issues in one cumulative
> patch made it harder to separate and review the purpose of each change.
> Considering the issues independently may also make it easier to identify a
> simpler approach for some of them. I hope these clarifications and test
> results are useful.
I'm sure they will be.
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,105 @@ 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 usb_ep *ep;
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;
+ bool is_read;
};
-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 usb_request *req;
struct ep_data *epdata;
- int value;
+ struct usb_ep *ep;
+ enum aio_req_state req_state;
- local_irq_disable();
+ priv = container_of(work, struct kiocb_priv, unlink_work);
+ req = priv->req;
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 = priv->ep;
- return value;
+ usb_ep_dequeue(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) {
+ /* priv may have been freed already */
+ usb_ep_free_request(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;
+ unsigned long flags;
+
+ spin_lock_irqsave(&aio_lock, flags);
+ priv = iocb->private;
+ if (!priv || priv->cancel_state != AIO_NOT_CANCELLED) {
+ spin_unlock_irqrestore(&aio_lock, flags);
+ return -EINVAL; /* Already completed or cancelled */
+ }
+ if (priv->req_state == AIO_SUBMITTING) {
+ priv->cancel_state = AIO_UNLINK_DONE;
+ spin_unlock_irqrestore(&aio_lock, flags);
+ return 0; /* ep_aio() will call us again if needed */
+ }
+
+ priv->cancel_state = AIO_UNLINKING;
+ spin_unlock_irqrestore(&aio_lock, flags);
+
+ /*
+ * 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,46 +549,62 @@ 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)
{
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
* complete the aio request immediately.
*/
- if (priv->to_free == NULL || unlikely(req->actual == 0)) {
+ if (!priv->is_read || 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))
- DBG(epdata->dev, "%s fault %d len %d\n",
+ DBG(priv->epdata->dev, "%s fault %d len %d\n",
ep->name, req->status, req->actual);
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(priv->epdata);
+ if (new_req_state == AIO_GIVEN_BACK)
+ kfree(priv);
+ }
}
static ssize_t ep_aio(struct kiocb *iocb,
@@ -532,11 +615,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 +631,12 @@ 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;
+ priv->ep = ep;
- 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 +646,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:
@@ -618,6 +737,7 @@ ep_read_iter(struct kiocb *iocb, struct
value = -ENOMEM;
if (!priv)
goto fail;
+ priv->is_read = true;
priv->to_free = dup_iter(&priv->to, to, GFP_KERNEL);
if (!iter_is_ubuf(&priv->to) && !priv->to_free) {
kfree(priv);
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-01 2:47 ` Alan Stern
@ 2026-09-04 14:00 ` Minseo Kim
2026-09-04 20:09 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-04 14:00 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the updated patch.
> If you trim your patch down to the parts that only target the AIO races,
> do you get anything significantly different from my version?
I compared your current patch with my AIO-only version and found no
significant difference in the AIO cancellation state machine or IRQ-state
handling.
Both versions snapshot epdata and ep alongside req before
usb_ep_dequeue(). In the directed cross-CPU test, a control based on your
current patch but without the local snapshots of epdata and ep reproduced
the earlier ep_unlink_worker() UAF; I did not reproduce it with your
current patch.
The only substantive implementation difference I found in the AIO-only
comparison was the criterion used alongside the req->actual check to
decide whether to queue copy work. My version used
iov_iter_count(&priv->to), whereas your current patch uses the explicit
is_read flag. I agree that is_read makes this decision more explicit and
avoids requiring the reader to infer it from how priv->to is initialized.
With your current patch, native PREAD and one- and two-segment PREADV
copied their payloads correctly. PWRITE, PWRITEV, and zero-length PREAD
and PWRITE completed without invoking ep_user_copy_worker().
I also reran the original null-ptr-deref and UAF reproducers, the exact
pre-queue cancellation matrix, and the reproducers for the earlier
candidate-patch regressions. I separately tested deferred giveback,
forced dequeue failure, and the earlier NULL-endpoint case. All of these
tests produced the expected results, and none of the corresponding
earlier failure signatures recurred.
One small point in the ep_aio_cancel() comment: as I understand it, the
completion function pointer in struct kiocb is named ki_complete, which
is also the name I used in my previous message. I wondered whether
iocb->kio_complete() was intended to be iocb->ki_complete().
Thank you again for updating the patch.
With great respect and appreciation,
Minseo Kim
2026년 9월 1일 (화) 오전 11:47, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Mon, Aug 31, 2026 at 10:30:08PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > Thank you for the comments and for pointing out where my explanation was
> > unclear.
> >
> > > Are you saying that this test in ep_read_iter():
> > >
> > > if (!iter_is_ubuf(&priv->to) && !priv->to_free) {
> > >
> > > is wrong, for example, the && should be || ?
> >
> > No. The && condition should remain as it is. dup_iter() copies the iterator
> > state into priv->to. In my tests, native PREAD and a one-segment PREADV used
> > ITER_UBUF. Since ITER_UBUF has no separate iovec array to duplicate,
> > dup_iter() returned NULL while the copied iterator remained valid. My
> > two-segment PREADV test used ITER_IOVEC; there, a NULL return would mean that
> > allocation of the duplicated iovec array failed. The iter_is_ubuf() check
> > therefore distinguishes the valid NULL return for ITER_UBUF from an allocation
> > failure in the ITER_IOVEC case. In a control build using ||, valid PREAD and
> > both one- and two-segment PREADV submissions failed with -ENOMEM.
> >
> > The issue I observed was instead in the later completion test:
> >
> > if (priv->to_free == NULL || unlikely(req->actual == 0)) {
> >
> > As I understand it, this later test treats a NULL to_free pointer as meaning
> > that no userspace copy is needed. In this path, however, to_free stores the
> > pointer returned by dup_iter() and tracks separately allocated iterator
> > backing, rather than whether the saved iterator has a destination.
>
> Okay, I see. I had assumed that the information about the userspace
> buffer was contained in the region that to_free points to.
>
> > For this issue, the relevant change was to base the later copy decision on
> > iov_iter_count(&priv->to); I did not change the ep_read_iter() condition. With
> > the original test, native PREAD reported res=511 while the destination buffer
> > remained unchanged. After this change, native PREAD and one-segment PREADV
> > copied the payload correctly, and two-segment PREADV continued to work.
>
> While that's a valid solution, it would be more straightforward to add
> an "is_read" flag to the priv structure. If an aio operation isn't a
> read then there certainly won't be anything to copy back to userspace
> upon completion. This doesn't require people reading the source to know
> anything about the internal details of struct iov_iter. And it is a
> better match to the immediately preceding comment.
>
> > My previous description of to_free as a predicate was inaccurate. I was
> > referring to its later completion-side use as a copy/no-copy indicator.
> >
> > > I understood that it was okay to call usb_ep_free_request() after
> > > usb_ep_disable(). Was that wrong? Did you see it create any problems?
> >
> > Your understanding matches the behavior I observed under dummy_hcd. For both
> > PWRITE and PREAD, I kept a completed, unqueued request allocated until after
> > usb_ep_disable() returned, with no intervening usb_ep_dequeue(). In each case,
> > the request completed exactly once with the expected result; the subsequent
> > usb_ep_free_request() produced no KASAN report or Oops.
> >
> > Separately, in an intermediate version I observed usb_ep_free_request()
> > running while usb_ep_disable() was still in progress. Because that version
> > moved normal request cleanup into a worker, I used the existing endpoint mutex
> > to prevent the worker's usb_ep_free_request() call from overlapping endpoint
> > disable while I investigated the ordering. The mutex eliminated the overlap,
> > but I did not reproduce a failure without it, so these tests did not establish
> > that the serialization was required.
>
> Good, so we don't need to worry about that.
>
> > The endpoint-use ordering I could reproduce in a directed dummy_hcd diagnostic
> > was different: without waiting for unlink work before endpoint disable,
> > ep_unlink_worker() could call usb_ep_dequeue() while usb_ep_disable() was in
> > progress. In matched PWRITE and PREAD tests, the variant without the
> > pre-disable unlink-work wait produced this overlap; with the wait retained,
> > usb_ep_dequeue() completed before usb_ep_disable() began.
> >
> > I did not reproduce a KASAN report, Oops, or userspace failure from the
> > concurrent dequeue/disable overlap itself. I retained the pre-disable wait in
> > the narrower test version as a conservative interpretation of the
> > usb_ep_disable() requirement that no other task be using the endpoint when it
> > is called.
>
> We don't need to be that conservative here. And in fact, I'm not
> entirely sure what that comment in usb_ep_disable() was intended to
> mean. Since the function doesn't cause the UDC driver's endpoint data
> structure to be deallocated, there isn't much to be careful of.
>
> > > This is probably because you were flushing the workqueues at the wrong
> > > time.
> >
> > Yes. In the callback-gate test, the relevant workqueue flush returned
> > while the completion callback was stopped before queueing its follow-up work.
> > In the narrower test version, I incremented aio_producers before
> > usb_ep_queue() and decremented it only after the callback had either queued
> > the required follow-up work or completed the immediate AIO result without
> > leaving deferred work to publish. If usb_ep_queue() failed, the submission
> > path decremented it directly. With that change, gadgetfs_unbind() remained
> > blocked in the producer wait while the callback was gated. It proceeded to
> > flush the completion workqueue only after the callback queued the follow-up
> > work.
> >
> > In the delayed-worker test, module unload completed before the running
> > unlink_work returned. This confirmed that module unload must not complete
> > while such work is still running.
> >
> > > For the final submission, I think the workqueue management stuff should
> > > go into its own separate patch.
> >
> > Separating the workqueue management changes from the AIO race fixes makes
> > sense. Thank you also for the reminder about -p.
>
> So let's worry about the workqueue stuff later and concentrate for now
> just on fixing the AIO races.
>
> > > Second, why did you change ep_aio_complete() to make it queue up
> > > ep_user_copy_worker() even when nothing needed to be copied to
> > > userspace?
> >
> > My reason for routing every completion through ep_user_copy_worker() was to
> > make a common deferred completion and request-cleanup stage visible to a
> > workqueue flush during teardown, not because every request needed a userspace
> > copy. In the normal non-unlink path of that cumulative design, the worker
> > released the USB request and epdata reference before calling ki_complete(),
> > and then published AIO_GIVEN_BACK. Since ki_complete() could drop the last AIO
> > file reference and allow ep_release() to begin, the intent was to prevent
> > teardown from passing the completion-work flush before that cleanup had
> > finished. A gate placed between cleanup and ki_complete() confirmed that the
> > flush covered this no-copy PWRITE cleanup: unbind reached
> > flush_workqueue(gadgetfs_copy_wq) but did not return until the worker was
> > released.
> >
> > I agree that using the worker for every completion was broader than necessary
> > for copy handling. The narrower behavior I tested retains aio_producers but
> > queues copy work only when iov_iter_count(&priv->to) and req->actual are both
> > nonzero. Nonzero PREAD and the one- and two-segment PREADV cases invoked the
> > copy worker, while PWRITE, PWRITEV, and zero-length reads and writes did not.
> > This indicates that no-copy paths do not need ep_user_copy_worker() merely for
> > copy handling. Their cleanup ordering and the separate teardown lifetime
> > issue can be considered independently of that routing decision.
>
> And since your decision was based on handling the workqueue flushing, it
> can be put off until later.
>
> If you trim your patch down to the parts that only target the AIO races,
> do you get anything significantly different from my version? I did see
> that you added or changed a few comments, and I have updated my version
> of the patch in a couple of respects, but overall yours must end up
> being pretty similar to mine. Please point out any differences you
> believe are worth mentioning. My current version is below.
>
> > Looking back, trying to address all of the observed issues in one cumulative
> > patch made it harder to separate and review the purpose of each change.
> > Considering the issues independently may also make it easier to identify a
> > simpler approach for some of them. I hope these clarifications and test
> > results are useful.
>
> I'm sure they will be.
>
> 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,105 @@ 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 usb_ep *ep;
> 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;
> + bool is_read;
> };
>
> -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 usb_request *req;
> struct ep_data *epdata;
> - int value;
> + struct usb_ep *ep;
> + enum aio_req_state req_state;
>
> - local_irq_disable();
> + priv = container_of(work, struct kiocb_priv, unlink_work);
> + req = priv->req;
> 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 = priv->ep;
>
> - return value;
> + usb_ep_dequeue(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) {
> + /* priv may have been freed already */
> + usb_ep_free_request(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;
> + unsigned long flags;
> +
> + spin_lock_irqsave(&aio_lock, flags);
> + priv = iocb->private;
> + if (!priv || priv->cancel_state != AIO_NOT_CANCELLED) {
> + spin_unlock_irqrestore(&aio_lock, flags);
> + return -EINVAL; /* Already completed or cancelled */
> + }
> + if (priv->req_state == AIO_SUBMITTING) {
> + priv->cancel_state = AIO_UNLINK_DONE;
> + spin_unlock_irqrestore(&aio_lock, flags);
> + return 0; /* ep_aio() will call us again if needed */
> + }
> +
> + priv->cancel_state = AIO_UNLINKING;
> + spin_unlock_irqrestore(&aio_lock, flags);
> +
> + /*
> + * 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,46 +549,62 @@ 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)
> {
> 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
> * complete the aio request immediately.
> */
> - if (priv->to_free == NULL || unlikely(req->actual == 0)) {
> + if (!priv->is_read || 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))
> - DBG(epdata->dev, "%s fault %d len %d\n",
> + DBG(priv->epdata->dev, "%s fault %d len %d\n",
> ep->name, req->status, req->actual);
>
> 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(priv->epdata);
> + if (new_req_state == AIO_GIVEN_BACK)
> + kfree(priv);
> + }
> }
>
> static ssize_t ep_aio(struct kiocb *iocb,
> @@ -532,11 +615,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 +631,12 @@ 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;
> + priv->ep = ep;
>
> - 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 +646,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:
> @@ -618,6 +737,7 @@ ep_read_iter(struct kiocb *iocb, struct
> value = -ENOMEM;
> if (!priv)
> goto fail;
> + priv->is_read = true;
> priv->to_free = dup_iter(&priv->to, to, GFP_KERNEL);
> if (!iter_is_ubuf(&priv->to) && !priv->to_free) {
> kfree(priv);
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-04 14:00 ` Minseo Kim
@ 2026-09-04 20:09 ` Alan Stern
2026-09-07 13:00 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-04 20:09 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Fri, Sep 04, 2026 at 11:00:23PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the updated patch.
>
> > If you trim your patch down to the parts that only target the AIO races,
> > do you get anything significantly different from my version?
>
> I compared your current patch with my AIO-only version and found no
> significant difference in the AIO cancellation state machine or IRQ-state
> handling.
>
> Both versions snapshot epdata and ep alongside req before
> usb_ep_dequeue(). In the directed cross-CPU test, a control based on your
> current patch but without the local snapshots of epdata and ep reproduced
> the earlier ep_unlink_worker() UAF; I did not reproduce it with your
> current patch.
>
> The only substantive implementation difference I found in the AIO-only
> comparison was the criterion used alongside the req->actual check to
> decide whether to queue copy work. My version used
> iov_iter_count(&priv->to), whereas your current patch uses the explicit
> is_read flag. I agree that is_read makes this decision more explicit and
> avoids requiring the reader to infer it from how priv->to is initialized.
>
> With your current patch, native PREAD and one- and two-segment PREADV
> copied their payloads correctly. PWRITE, PWRITEV, and zero-length PREAD
> and PWRITE completed without invoking ep_user_copy_worker().
>
> I also reran the original null-ptr-deref and UAF reproducers, the exact
> pre-queue cancellation matrix, and the reproducers for the earlier
> candidate-patch regressions. I separately tested deferred giveback,
> forced dequeue failure, and the earlier NULL-endpoint case. All of these
> tests produced the expected results, and none of the corresponding
> earlier failure signatures recurred.
>
> One small point in the ep_aio_cancel() comment: as I understand it, the
> completion function pointer in struct kiocb is named ki_complete, which
> is also the name I used in my previous message. I wondered whether
> iocb->kio_complete() was intended to be iocb->ki_complete().
Yes, that was simply a typo. Thanks for spotting it.
> Thank you again for updating the patch.
You're welcome. And now it's time to submit this patch. Is it okay
to add your Tested-by: tag?
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-04 20:09 ` Alan Stern
@ 2026-09-07 13:00 ` Minseo Kim
2026-09-08 19:02 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-07 13:00 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
> Is it okay to add your Tested-by: tag?
Yes, please add:
Tested-by: Minseo Kim <neck3922@gmail.com>
Thank you for the time and care you have put into this fix. I have learned
a great deal while working through this issue with you.
Best regards,
Minseo Kim
2026년 9월 5일 (토) 오전 5:10, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Fri, Sep 04, 2026 at 11:00:23PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > Thank you for the updated patch.
> >
> > > If you trim your patch down to the parts that only target the AIO races,
> > > do you get anything significantly different from my version?
> >
> > I compared your current patch with my AIO-only version and found no
> > significant difference in the AIO cancellation state machine or IRQ-state
> > handling.
> >
> > Both versions snapshot epdata and ep alongside req before
> > usb_ep_dequeue(). In the directed cross-CPU test, a control based on your
> > current patch but without the local snapshots of epdata and ep reproduced
> > the earlier ep_unlink_worker() UAF; I did not reproduce it with your
> > current patch.
> >
> > The only substantive implementation difference I found in the AIO-only
> > comparison was the criterion used alongside the req->actual check to
> > decide whether to queue copy work. My version used
> > iov_iter_count(&priv->to), whereas your current patch uses the explicit
> > is_read flag. I agree that is_read makes this decision more explicit and
> > avoids requiring the reader to infer it from how priv->to is initialized.
> >
> > With your current patch, native PREAD and one- and two-segment PREADV
> > copied their payloads correctly. PWRITE, PWRITEV, and zero-length PREAD
> > and PWRITE completed without invoking ep_user_copy_worker().
> >
> > I also reran the original null-ptr-deref and UAF reproducers, the exact
> > pre-queue cancellation matrix, and the reproducers for the earlier
> > candidate-patch regressions. I separately tested deferred giveback,
> > forced dequeue failure, and the earlier NULL-endpoint case. All of these
> > tests produced the expected results, and none of the corresponding
> > earlier failure signatures recurred.
> >
> > One small point in the ep_aio_cancel() comment: as I understand it, the
> > completion function pointer in struct kiocb is named ki_complete, which
> > is also the name I used in my previous message. I wondered whether
> > iocb->kio_complete() was intended to be iocb->ki_complete().
>
> Yes, that was simply a typo. Thanks for spotting it.
>
> > Thank you again for updating the patch.
>
> You're welcome. And now it's time to submit this patch. Is it okay
> to add your Tested-by: tag?
>
> Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-07 13:00 ` Minseo Kim
@ 2026-09-08 19:02 ` Alan Stern
2026-09-10 14:00 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-08 19:02 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Mon, Sep 07, 2026 at 10:00:00PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> > Is it okay to add your Tested-by: tag?
>
> Yes, please add:
>
> Tested-by: Minseo Kim <neck3922@gmail.com>
Will do.
> Thank you for the time and care you have put into this fix. I have learned
> a great deal while working through this issue with you.
You're welcome.
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?
This patch introduces the possibility of that happening. Therefore I
would like to have a second patch, which flushes the workqueue and
prevents the kernel from trying to run code in an unloaded module, ready
to submit along with the first one.
My version of this second patch (meant to apply on top of the first
patch) is below. It is closely based on the version you wrote, the main
difference being that it uses a single workqueue for both work routines.
Could you test it and verify that it prevents the problem you observed
with only the first patch installed?
Thank,
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
@@ -237,6 +237,7 @@ static const char *CHIP;
static DEFINE_MUTEX(sb_mutex); /* Serialize superblock operations */
static DEFINE_SPINLOCK(aio_lock); /* Protect aio cancellation info */
+static struct workqueue_struct *gadgetfs_wq;
/*----------------------------------------------------------------------*/
@@ -514,9 +515,7 @@ static int ep_aio_cancel(struct kiocb *i
spin_unlock_irqrestore(&aio_lock, flags);
return 0; /* ep_aio() will call us again if needed */
}
-
priv->cancel_state = AIO_UNLINKING;
- spin_unlock_irqrestore(&aio_lock, flags);
/*
* We are called with the aio core holding iocb's context lock.
@@ -527,7 +526,9 @@ static int ep_aio_cancel(struct kiocb *i
* For this reason, do the dequeue operation in a work routine.
*/
INIT_WORK(&priv->unlink_work, ep_unlink_worker);
- schedule_work(&priv->unlink_work);
+ queue_work(gadgetfs_wq, &priv->unlink_work);
+
+ spin_unlock_irqrestore(&aio_lock, flags);
return 0;
}
@@ -597,7 +598,7 @@ static void ep_aio_complete(struct usb_e
if (new_req_state == AIO_COMPLETED) {
INIT_WORK(&priv->copy_work, ep_user_copy_worker);
- schedule_work(&priv->copy_work);
+ queue_work(gadgetfs_wq, &priv->copy_work);
}
if (cancel_state != AIO_UNLINKING) {
usb_ep_free_request(ep, req);
@@ -1773,6 +1774,8 @@ gadgetfs_unbind (struct usb_gadget *gadg
spin_unlock_irq (&dev->lock);
destroy_ep_files (dev);
+ flush_workqueue(gadgetfs_wq);
+
gadget->ep0->driver_data = NULL;
set_gadget_data (gadget, NULL);
@@ -2245,10 +2248,17 @@ static int __init gadgetfs_init (void)
{
int status;
+ gadgetfs_wq = alloc_workqueue("gadgetfs", WQ_PERCPU, 0);
+ if (!gadgetfs_wq)
+ return -ENOMEM;
+
status = register_filesystem (&gadgetfs_type);
- if (status == 0)
- pr_info ("%s: %s, version " DRIVER_VERSION "\n",
- shortname, driver_desc);
+ if (status) {
+ destroy_workqueue(gadgetfs_wq);
+ return status;
+ }
+
+ pr_info("%s: %s, version " DRIVER_VERSION "\n", shortname, driver_desc);
return status;
}
module_init (gadgetfs_init);
@@ -2257,6 +2267,7 @@ static void __exit gadgetfs_cleanup (voi
{
pr_debug ("unregister %s\n", shortname);
unregister_filesystem (&gadgetfs_type);
+ destroy_workqueue(gadgetfs_wq);
}
module_exit (gadgetfs_cleanup);
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-08 19:02 ` Alan Stern
@ 2026-09-10 14:00 ` Minseo Kim
2026-09-10 15:50 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-10 14:00 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
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().
> 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. 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.
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.
With sincere appreciation and great respect,
Minseo Kim
2026년 9월 9일 (수) 오전 4:03, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Mon, Sep 07, 2026 at 10:00:00PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > > Is it okay to add your Tested-by: tag?
> >
> > Yes, please add:
> >
> > Tested-by: Minseo Kim <neck3922@gmail.com>
>
> Will do.
>
> > Thank you for the time and care you have put into this fix. I have learned
> > a great deal while working through this issue with you.
>
> You're welcome.
>
> 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?
>
> This patch introduces the possibility of that happening. Therefore I
> would like to have a second patch, which flushes the workqueue and
> prevents the kernel from trying to run code in an unloaded module, ready
> to submit along with the first one.
>
> My version of this second patch (meant to apply on top of the first
> patch) is below. It is closely based on the version you wrote, the main
> difference being that it uses a single workqueue for both work routines.
> Could you test it and verify that it prevents the problem you observed
> with only the first patch installed?
>
> Thank,
>
> 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
> @@ -237,6 +237,7 @@ static const char *CHIP;
> static DEFINE_MUTEX(sb_mutex); /* Serialize superblock operations */
>
> static DEFINE_SPINLOCK(aio_lock); /* Protect aio cancellation info */
> +static struct workqueue_struct *gadgetfs_wq;
>
> /*----------------------------------------------------------------------*/
>
> @@ -514,9 +515,7 @@ static int ep_aio_cancel(struct kiocb *i
> spin_unlock_irqrestore(&aio_lock, flags);
> return 0; /* ep_aio() will call us again if needed */
> }
> -
> priv->cancel_state = AIO_UNLINKING;
> - spin_unlock_irqrestore(&aio_lock, flags);
>
> /*
> * We are called with the aio core holding iocb's context lock.
> @@ -527,7 +526,9 @@ static int ep_aio_cancel(struct kiocb *i
> * For this reason, do the dequeue operation in a work routine.
> */
> INIT_WORK(&priv->unlink_work, ep_unlink_worker);
> - schedule_work(&priv->unlink_work);
> + queue_work(gadgetfs_wq, &priv->unlink_work);
> +
> + spin_unlock_irqrestore(&aio_lock, flags);
> return 0;
> }
>
> @@ -597,7 +598,7 @@ static void ep_aio_complete(struct usb_e
>
> if (new_req_state == AIO_COMPLETED) {
> INIT_WORK(&priv->copy_work, ep_user_copy_worker);
> - schedule_work(&priv->copy_work);
> + queue_work(gadgetfs_wq, &priv->copy_work);
> }
> if (cancel_state != AIO_UNLINKING) {
> usb_ep_free_request(ep, req);
> @@ -1773,6 +1774,8 @@ gadgetfs_unbind (struct usb_gadget *gadg
> spin_unlock_irq (&dev->lock);
>
> destroy_ep_files (dev);
> + flush_workqueue(gadgetfs_wq);
> +
> gadget->ep0->driver_data = NULL;
> set_gadget_data (gadget, NULL);
>
> @@ -2245,10 +2248,17 @@ static int __init gadgetfs_init (void)
> {
> int status;
>
> + gadgetfs_wq = alloc_workqueue("gadgetfs", WQ_PERCPU, 0);
> + if (!gadgetfs_wq)
> + return -ENOMEM;
> +
> status = register_filesystem (&gadgetfs_type);
> - if (status == 0)
> - pr_info ("%s: %s, version " DRIVER_VERSION "\n",
> - shortname, driver_desc);
> + if (status) {
> + destroy_workqueue(gadgetfs_wq);
> + return status;
> + }
> +
> + pr_info("%s: %s, version " DRIVER_VERSION "\n", shortname, driver_desc);
> return status;
> }
> module_init (gadgetfs_init);
> @@ -2257,6 +2267,7 @@ static void __exit gadgetfs_cleanup (voi
> {
> pr_debug ("unregister %s\n", shortname);
> unregister_filesystem (&gadgetfs_type);
> + destroy_workqueue(gadgetfs_wq);
> }
> module_exit (gadgetfs_cleanup);
>
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-10 14:00 ` Minseo Kim
@ 2026-09-10 15:50 ` Alan Stern
2026-09-11 14:00 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-10 15:50 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
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
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-10 15:50 ` Alan Stern
@ 2026-09-11 14:00 ` Minseo Kim
2026-09-11 19:22 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-11 14:00 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
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@rowland.harvard.edu>님이 작성:
>
> 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
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-11 14:00 ` Minseo Kim
@ 2026-09-11 19:22 ` Alan Stern
2026-09-14 14:00 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-11 19:22 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Fri, Sep 11, 2026 at 11:00:00PM +0900, Minseo Kim wrote:
> 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.
Aha! In other words, usb_ep_disable() doesn't guarantee that the
completion handlers for all outstanding requests on the endpoint have
finished -- it only guarantees that they have started. That's an
important difference. Guess I should update the documentation for
that routine.
> 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.
These results explain a lot. They mean that flushing the workqueue in
gadgetfs_unbind() not only doesn't do what we want, it also isn't
necessary -- because destroy_workqueue() will automatically do a flush
for us.
Also, it isn't really necessary to flush the workqueue when the user
program closes ep0 or unmounts the gadgetfs filesystem. We merely have
to make sure that when the module is unloaded, no work items are running
or will be started.
Here's a revised version of the second patch, which omits the
flush_workqueue() call from gadgetfs_unbind(). I expect it will
eliminate the "module unloaded while work items are still running"
problem just as well as the original version did.
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
@@ -237,6 +237,7 @@ static const char *CHIP;
static DEFINE_MUTEX(sb_mutex); /* Serialize superblock operations */
static DEFINE_SPINLOCK(aio_lock); /* Protect aio cancellation info */
+static struct workqueue_struct *gadgetfs_wq;
/*----------------------------------------------------------------------*/
@@ -514,9 +515,7 @@ static int ep_aio_cancel(struct kiocb *i
spin_unlock_irqrestore(&aio_lock, flags);
return 0; /* ep_aio() will call us again if needed */
}
-
priv->cancel_state = AIO_UNLINKING;
- spin_unlock_irqrestore(&aio_lock, flags);
/*
* We are called with the aio core holding iocb's context lock.
@@ -527,7 +526,9 @@ static int ep_aio_cancel(struct kiocb *i
* For this reason, do the dequeue operation in a work routine.
*/
INIT_WORK(&priv->unlink_work, ep_unlink_worker);
- schedule_work(&priv->unlink_work);
+ queue_work(gadgetfs_wq, &priv->unlink_work);
+
+ spin_unlock_irqrestore(&aio_lock, flags);
return 0;
}
@@ -597,7 +598,7 @@ static void ep_aio_complete(struct usb_e
if (new_req_state == AIO_COMPLETED) {
INIT_WORK(&priv->copy_work, ep_user_copy_worker);
- schedule_work(&priv->copy_work);
+ queue_work(gadgetfs_wq, &priv->copy_work);
}
if (cancel_state != AIO_UNLINKING) {
usb_ep_free_request(ep, req);
@@ -2245,10 +2246,17 @@ static int __init gadgetfs_init (void)
{
int status;
+ gadgetfs_wq = alloc_workqueue("gadgetfs", WQ_PERCPU, 0);
+ if (!gadgetfs_wq)
+ return -ENOMEM;
+
status = register_filesystem (&gadgetfs_type);
- if (status == 0)
- pr_info ("%s: %s, version " DRIVER_VERSION "\n",
- shortname, driver_desc);
+ if (status) {
+ destroy_workqueue(gadgetfs_wq);
+ return status;
+ }
+
+ pr_info("%s: %s, version " DRIVER_VERSION "\n", shortname, driver_desc);
return status;
}
module_init (gadgetfs_init);
@@ -2257,6 +2265,7 @@ static void __exit gadgetfs_cleanup (voi
{
pr_debug ("unregister %s\n", shortname);
unregister_filesystem (&gadgetfs_type);
+ destroy_workqueue(gadgetfs_wq);
}
module_exit (gadgetfs_cleanup);
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-11 19:22 ` Alan Stern
@ 2026-09-14 14:00 ` Minseo Kim
2026-09-14 16:00 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-14 14:00 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the revised patch.
Following your explanation, I focused this round of testing on how the
revised patch handles module lifetime.
I applied your first patch followed by this revised second patch to
upstream v7.2-rc1, commit
dc59e4fea9d83f03bad6bddf3fa2e52491777482, using
CONFIG_USB_GADGETFS=m for the runtime tests.
With the unbind flush removed, I held ep_unlink_worker() at entry.
gadgetfs_unbind() returned and the module usage count reached zero, but
rmmod remained blocked in gadgetfs_cleanup()'s destroy_workqueue(). After
I released the worker, rmmod completed successfully. Holding
ep_user_copy_worker() immediately after iocb->ki_complete() likewise kept
rmmod blocked in destroy_workqueue() until I released the worker. In both
cases, the blocked rmmod stack showed __flush_workqueue(),
drain_workqueue(), destroy_workqueue(), and gadgetfs_cleanup().
To check whether module unloading could begin before a callback published
its work, I held the completion callback immediately before it queued
copy_work. At that point, the reproducer's file descriptor table contained
no GadgetFS descriptors. In two runs, each starting from a fresh boot, a
direct umount2() call failed with errno set to EBUSY while the callback was
held. The module usage count remained 2, rmmod was rejected because the
module was in use, and gadgetfs_cleanup() did not begin. After I released
the callback, it queued copy_work, the AIO request completed, and subsequent
unmount and rmmod attempts succeeded.
I also held ep_aio_complete() immediately after iocb->ki_complete() on
the PWRITE path that does not queue copy_work. In two runs, each starting
from a fresh boot, after the reproducer consumed the AIO completion event
and closed all GadgetFS descriptors, unmount remained pending, at least
one concurrent delete_module() attempt failed with errno set to
EWOULDBLOCK, and gadgetfs_cleanup() did not begin while the callback tail
was held. After I released the gate, the callback returned, unmount
completed, and module removal succeeded.
In another cancellation test, both unlink_work and copy_work were
outstanding on gadgetfs_wq when rmmod entered destroy_workqueue().
Releasing unlink_work alone did not allow cleanup to return; it returned
only after I released copy_work.
Using the GadgetFS source with both patches applied and without the
diagnostic gates or markers, I reran the original NULL pointer dereference
and UAF reproducers and the relevant AIO cancellation, payload, teardown,
CPU hotplug, rebind, module reload, and partial read stress tests. All
completed with the expected results and without a KASAN report, Oops, or
LOCKDEP warning.
In these x86-64 QEMU and dummy_hcd tests, module cleanup did not begin while
a callback still had work to publish, and destroy_workqueue() waited for the
remaining GadgetFS AIO work once module cleanup began. I did not find a new
failure attributable to the revised second patch in these tests.
Thank you for examining this issue with such care and for the time and
effort you have devoted to it.
With sincere appreciation and great respect,
Minseo Kim
2026년 9월 12일 (토) 오전 4:22, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Fri, Sep 11, 2026 at 11:00:00PM +0900, Minseo Kim wrote:
> > 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.
>
> Aha! In other words, usb_ep_disable() doesn't guarantee that the
> completion handlers for all outstanding requests on the endpoint have
> finished -- it only guarantees that they have started. That's an
> important difference. Guess I should update the documentation for
> that routine.
>
> > 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.
>
> These results explain a lot. They mean that flushing the workqueue in
> gadgetfs_unbind() not only doesn't do what we want, it also isn't
> necessary -- because destroy_workqueue() will automatically do a flush
> for us.
>
> Also, it isn't really necessary to flush the workqueue when the user
> program closes ep0 or unmounts the gadgetfs filesystem. We merely have
> to make sure that when the module is unloaded, no work items are running
> or will be started.
>
> Here's a revised version of the second patch, which omits the
> flush_workqueue() call from gadgetfs_unbind(). I expect it will
> eliminate the "module unloaded while work items are still running"
> problem just as well as the original version did.
>
> 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
> @@ -237,6 +237,7 @@ static const char *CHIP;
> static DEFINE_MUTEX(sb_mutex); /* Serialize superblock operations */
>
> static DEFINE_SPINLOCK(aio_lock); /* Protect aio cancellation info */
> +static struct workqueue_struct *gadgetfs_wq;
>
> /*----------------------------------------------------------------------*/
>
> @@ -514,9 +515,7 @@ static int ep_aio_cancel(struct kiocb *i
> spin_unlock_irqrestore(&aio_lock, flags);
> return 0; /* ep_aio() will call us again if needed */
> }
> -
> priv->cancel_state = AIO_UNLINKING;
> - spin_unlock_irqrestore(&aio_lock, flags);
>
> /*
> * We are called with the aio core holding iocb's context lock.
> @@ -527,7 +526,9 @@ static int ep_aio_cancel(struct kiocb *i
> * For this reason, do the dequeue operation in a work routine.
> */
> INIT_WORK(&priv->unlink_work, ep_unlink_worker);
> - schedule_work(&priv->unlink_work);
> + queue_work(gadgetfs_wq, &priv->unlink_work);
> +
> + spin_unlock_irqrestore(&aio_lock, flags);
> return 0;
> }
>
> @@ -597,7 +598,7 @@ static void ep_aio_complete(struct usb_e
>
> if (new_req_state == AIO_COMPLETED) {
> INIT_WORK(&priv->copy_work, ep_user_copy_worker);
> - schedule_work(&priv->copy_work);
> + queue_work(gadgetfs_wq, &priv->copy_work);
> }
> if (cancel_state != AIO_UNLINKING) {
> usb_ep_free_request(ep, req);
> @@ -2245,10 +2246,17 @@ static int __init gadgetfs_init (void)
> {
> int status;
>
> + gadgetfs_wq = alloc_workqueue("gadgetfs", WQ_PERCPU, 0);
> + if (!gadgetfs_wq)
> + return -ENOMEM;
> +
> status = register_filesystem (&gadgetfs_type);
> - if (status == 0)
> - pr_info ("%s: %s, version " DRIVER_VERSION "\n",
> - shortname, driver_desc);
> + if (status) {
> + destroy_workqueue(gadgetfs_wq);
> + return status;
> + }
> +
> + pr_info("%s: %s, version " DRIVER_VERSION "\n", shortname, driver_desc);
> return status;
> }
> module_init (gadgetfs_init);
> @@ -2257,6 +2265,7 @@ static void __exit gadgetfs_cleanup (voi
> {
> pr_debug ("unregister %s\n", shortname);
> unregister_filesystem (&gadgetfs_type);
> + destroy_workqueue(gadgetfs_wq);
> }
> module_exit (gadgetfs_cleanup);
>
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-14 14:00 ` Minseo Kim
@ 2026-09-14 16:00 ` Alan Stern
2026-09-16 14:00 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-14 16:00 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Mon, Sep 14, 2026 at 11:00:00PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the revised patch.
>
> Following your explanation, I focused this round of testing on how the
> revised patch handles module lifetime.
>
> I applied your first patch followed by this revised second patch to
> upstream v7.2-rc1, commit
> dc59e4fea9d83f03bad6bddf3fa2e52491777482, using
> CONFIG_USB_GADGETFS=m for the runtime tests.
...
> I also held ep_aio_complete() immediately after iocb->ki_complete() on
> the PWRITE path that does not queue copy_work. In two runs, each starting
> from a fresh boot, after the reproducer consumed the AIO completion event
> and closed all GadgetFS descriptors, unmount remained pending, at least
> one concurrent delete_module() attempt failed with errno set to
> EWOULDBLOCK, and gadgetfs_cleanup() did not begin while the callback tail
> was held. After I released the gate, the callback returned, unmount
> completed, and module removal succeeded.
Hmmm. Do you know where the unmount operation was getting stuck? Was
it the usb_gadget_unregister_driver() call inside dev_release()? I just
want to be sure about this.
> In another cancellation test, both unlink_work and copy_work were
> outstanding on gadgetfs_wq when rmmod entered destroy_workqueue().
> Releasing unlink_work alone did not allow cleanup to return; it returned
> only after I released copy_work.
>
> Using the GadgetFS source with both patches applied and without the
> diagnostic gates or markers, I reran the original NULL pointer dereference
> and UAF reproducers and the relevant AIO cancellation, payload, teardown,
> CPU hotplug, rebind, module reload, and partial read stress tests. All
> completed with the expected results and without a KASAN report, Oops, or
> LOCKDEP warning.
>
> In these x86-64 QEMU and dummy_hcd tests, module cleanup did not begin while
> a callback still had work to publish, and destroy_workqueue() waited for the
> remaining GadgetFS AIO work once module cleanup began. I did not find a new
> failure attributable to the revised second patch in these tests.
That all sounds very good.
> Thank you for examining this issue with such care and for the time and
> effort you have devoted to it.
And the same to you.
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-14 16:00 ` Alan Stern
@ 2026-09-16 14:00 ` Minseo Kim
2026-09-16 19:39 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-16 14:00 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
> Hmmm. Do you know where the unmount operation was getting stuck? Was
> it the usb_gadget_unregister_driver() call inside dev_release()? I just
> want to be sure about this.
In these reruns, I found that the unmount task was blocked in
synchronize_rcu_expedited(), called from namespace_unlock(), rather than
in usb_gadget_unregister_driver().
I reran the PWRITE callback tail test with both patches applied, using a
resident helper whose main thread invoked umount2() directly. A monitor
thread in the helper captured the blocked main thread's kernel stack. In
three runs from fresh boots, the relevant frames were:
synchronize_rcu_expedited
namespace_unlock
path_umount
__x64_sys_umount
In each of those three runs, the same umount2() call returned
successfully after I released the callback gate.
USB gadget request completion callbacks run with interrupts disabled, and
interrupt-disabled regions act as implicit RCU read-side critical
sections. The observed wait is therefore consistent with the diagnostic
gate delaying completion of the expedited grace period.
I also added diagnostic markers around dev_release() and
usb_gadget_unregister_driver(). The test harness signaled the resident
helper only after the reproducer had closed ep0. In five clean runs from
fresh boots with these markers, both usb_gadget_unregister_driver() and
dev_release() had returned before the resident helper invoked umount2(),
while ep_aio_complete() was still held immediately after
iocb->ki_complete().
The diagnostic gate used a bounded loop that did not sleep in the
dummy_hcd completion context. In three control runs with that gate
disabled, umount2() returned successfully without the prolonged wait seen
in the gated runs. The blocked task's stack identifies the wait point,
while the markers show that both usb_gadget_unregister_driver() and
dev_release() had already returned.
Thank you for asking me to verify this.
With sincere appreciation and great respect,
Minseo Kim
2026년 9월 15일 (화) 오전 1:01, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Mon, Sep 14, 2026 at 11:00:00PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > Thank you for the revised patch.
> >
> > Following your explanation, I focused this round of testing on how the
> > revised patch handles module lifetime.
> >
> > I applied your first patch followed by this revised second patch to
> > upstream v7.2-rc1, commit
> > dc59e4fea9d83f03bad6bddf3fa2e52491777482, using
> > CONFIG_USB_GADGETFS=m for the runtime tests.
>
> ...
>
> > I also held ep_aio_complete() immediately after iocb->ki_complete() on
> > the PWRITE path that does not queue copy_work. In two runs, each starting
> > from a fresh boot, after the reproducer consumed the AIO completion event
> > and closed all GadgetFS descriptors, unmount remained pending, at least
> > one concurrent delete_module() attempt failed with errno set to
> > EWOULDBLOCK, and gadgetfs_cleanup() did not begin while the callback tail
> > was held. After I released the gate, the callback returned, unmount
> > completed, and module removal succeeded.
>
> Hmmm. Do you know where the unmount operation was getting stuck? Was
> it the usb_gadget_unregister_driver() call inside dev_release()? I just
> want to be sure about this.
>
> > In another cancellation test, both unlink_work and copy_work were
> > outstanding on gadgetfs_wq when rmmod entered destroy_workqueue().
> > Releasing unlink_work alone did not allow cleanup to return; it returned
> > only after I released copy_work.
> >
> > Using the GadgetFS source with both patches applied and without the
> > diagnostic gates or markers, I reran the original NULL pointer dereference
> > and UAF reproducers and the relevant AIO cancellation, payload, teardown,
> > CPU hotplug, rebind, module reload, and partial read stress tests. All
> > completed with the expected results and without a KASAN report, Oops, or
> > LOCKDEP warning.
> >
> > In these x86-64 QEMU and dummy_hcd tests, module cleanup did not begin while
> > a callback still had work to publish, and destroy_workqueue() waited for the
> > remaining GadgetFS AIO work once module cleanup began. I did not find a new
> > failure attributable to the revised second patch in these tests.
>
> That all sounds very good.
>
> > Thank you for examining this issue with such care and for the time and
> > effort you have devoted to it.
>
> And the same to you.
>
> Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-16 14:00 ` Minseo Kim
@ 2026-09-16 19:39 ` Alan Stern
2026-09-17 14:02 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-16 19:39 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Wed, Sep 16, 2026 at 11:00:00PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> > Hmmm. Do you know where the unmount operation was getting stuck? Was
> > it the usb_gadget_unregister_driver() call inside dev_release()? I just
> > want to be sure about this.
>
> In these reruns, I found that the unmount task was blocked in
> synchronize_rcu_expedited(), called from namespace_unlock(), rather than
> in usb_gadget_unregister_driver().
>
> I reran the PWRITE callback tail test with both patches applied, using a
> resident helper whose main thread invoked umount2() directly. A monitor
> thread in the helper captured the blocked main thread's kernel stack. In
> three runs from fresh boots, the relevant frames were:
>
> synchronize_rcu_expedited
> namespace_unlock
> path_umount
> __x64_sys_umount
>
> In each of those three runs, the same umount2() call returned
> successfully after I released the callback gate.
>
> USB gadget request completion callbacks run with interrupts disabled, and
> interrupt-disabled regions act as implicit RCU read-side critical
> sections. The observed wait is therefore consistent with the diagnostic
> gate delaying completion of the expedited grace period.
>
> I also added diagnostic markers around dev_release() and
> usb_gadget_unregister_driver(). The test harness signaled the resident
> helper only after the reproducer had closed ep0. In five clean runs from
> fresh boots with these markers, both usb_gadget_unregister_driver() and
> dev_release() had returned before the resident helper invoked umount2(),
> while ep_aio_complete() was still held immediately after
> iocb->ki_complete().
Okay, that's not good. We can't expect to rely on an unintended side
effect. All the completion handlers should finish before
usb_gadget_unregister_driver() returns; we need to enforce that.
I believe this misbehavior is caused by an oversight in the dummy-hcd
driver. Can you repeat these tests with the patch below applied on top
of the other two? It should cause usb_gadget_unregister_driver() to
wait until ep_aio_complete() returns (the actual wait loop is in
dummy-hcd's dummy_udc_async_callbacks() routine).
Alan Stern
Index: usb-devel/drivers/usb/gadget/udc/dummy_hcd.c
===================================================================
--- usb-devel.orig/drivers/usb/gadget/udc/dummy_hcd.c
+++ usb-devel/drivers/usb/gadget/udc/dummy_hcd.c
@@ -343,9 +343,11 @@ static void dummy_giveback(struct dummy
{
bool fifo = req == &dum->fifo_req;
+ ++dum->callback_usage;
spin_unlock(&dum->lock);
usb_gadget_giveback_request(_ep, &req->req);
spin_lock(&dum->lock);
+ --dum->callback_usage;
if (fifo)
dum->fifo_req_busy = 0;
}
@@ -759,11 +761,13 @@ static int dummy_queue(struct usb_ep *_e
req->req.complete = fifo_complete;
list_add_tail(&req->queue, &ep->queue);
+ ++dum->callback_usage;
spin_unlock(&dum->lock);
_req->actual = _req->length;
_req->status = 0;
usb_gadget_giveback_request(_ep, _req);
spin_lock(&dum->lock);
+ --dum->callback_usage;
} else
list_add_tail(&req->queue, &ep->queue);
spin_unlock_irqrestore(&dum->lock, flags);
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-16 19:39 ` Alan Stern
@ 2026-09-17 14:02 ` Minseo Kim
2026-09-17 15:38 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-17 14:02 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
Thank you for the patch.
> Can you repeat these tests with the patch below applied on top
> of the other two? It should cause usb_gadget_unregister_driver() to
> wait until ep_aio_complete() returns (the actual wait loop is in
> dummy-hcd's dummy_udc_async_callbacks() routine).
Yes. With the new dummy_hcd change applied on top of the two GadgetFS
patches, usb_gadget_unregister_driver() waited in
dummy_udc_async_callbacks() until the held ep_aio_complete() callback
returned in both the normal transfer path through dummy_giveback() and
the direct giveback path in dummy_queue().
I used upstream v7.2-rc1 as the base. Because dummy_giveback() is not
present in v7.2-rc1, both matched test trees also included the changes
from upstream commit d5e5cd3654d2b5359a12ea6586120f05b28634ee
("usb: gadget: dummy_hcd: prevent fifo_req reuse during giveback").
In the normal transfer case, I held ep_aio_complete() immediately after
iocb->ki_complete(). Without the new dummy_hcd change,
dummy_udc_async_callbacks(false) saw callback_usage equal to 0, and the
ep0 close returned while the callback was still held. With the change,
it saw callback_usage equal to 1, and the ep0 close task remained blocked
in dummy_udc_async_callbacks(). The relevant part of the blocked close
task's stack was:
dummy_udc_async_callbacks [dummy_hcd]
gadget_unbind_driver
device_remove
device_release_driver_internal
driver_detach
bus_remove_driver
driver_unregister
usb_gadget_unregister_driver
dev_release [gadgetfs]
After I released the callback gate, ep_aio_complete() returned,
callback_usage fell to 0, and the same close completed. I observed this
sequence in four separate runs with the new change.
To exercise the other accounting site, I separately tested the direct
giveback path for a 64-byte write in dummy_queue(). With the change, the
close task again waited in dummy_udc_async_callbacks() with callback_usage
equal to 1. In the matched control, dummy_udc_async_callbacks() returned
with callback_usage at 0, and gadgetfs_unbind() then remained in its
existing udc_usage wait until I released the callback gate.
I also repeated the case where the request was still on the endpoint
queue when usb_ep_disable() began. Neither usb_ep_disable() nor the ep0
close returned while the completion callback was held. After I released
the callback gate, io_getevents() returned one completion with
res=-ESHUTDOWN, and a subsequent check returned no additional completion
event.
With sincere appreciation and great respect,
Minseo Kim
2026년 9월 17일 (목) 오전 4:39, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Wed, Sep 16, 2026 at 11:00:00PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > > Hmmm. Do you know where the unmount operation was getting stuck? Was
> > > it the usb_gadget_unregister_driver() call inside dev_release()? I just
> > > want to be sure about this.
> >
> > In these reruns, I found that the unmount task was blocked in
> > synchronize_rcu_expedited(), called from namespace_unlock(), rather than
> > in usb_gadget_unregister_driver().
> >
> > I reran the PWRITE callback tail test with both patches applied, using a
> > resident helper whose main thread invoked umount2() directly. A monitor
> > thread in the helper captured the blocked main thread's kernel stack. In
> > three runs from fresh boots, the relevant frames were:
> >
> > synchronize_rcu_expedited
> > namespace_unlock
> > path_umount
> > __x64_sys_umount
> >
> > In each of those three runs, the same umount2() call returned
> > successfully after I released the callback gate.
> >
> > USB gadget request completion callbacks run with interrupts disabled, and
> > interrupt-disabled regions act as implicit RCU read-side critical
> > sections. The observed wait is therefore consistent with the diagnostic
> > gate delaying completion of the expedited grace period.
> >
> > I also added diagnostic markers around dev_release() and
> > usb_gadget_unregister_driver(). The test harness signaled the resident
> > helper only after the reproducer had closed ep0. In five clean runs from
> > fresh boots with these markers, both usb_gadget_unregister_driver() and
> > dev_release() had returned before the resident helper invoked umount2(),
> > while ep_aio_complete() was still held immediately after
> > iocb->ki_complete().
>
> Okay, that's not good. We can't expect to rely on an unintended side
> effect. All the completion handlers should finish before
> usb_gadget_unregister_driver() returns; we need to enforce that.
>
> I believe this misbehavior is caused by an oversight in the dummy-hcd
> driver. Can you repeat these tests with the patch below applied on top
> of the other two? It should cause usb_gadget_unregister_driver() to
> wait until ep_aio_complete() returns (the actual wait loop is in
> dummy-hcd's dummy_udc_async_callbacks() routine).
>
> Alan Stern
>
>
> Index: usb-devel/drivers/usb/gadget/udc/dummy_hcd.c
> ===================================================================
> --- usb-devel.orig/drivers/usb/gadget/udc/dummy_hcd.c
> +++ usb-devel/drivers/usb/gadget/udc/dummy_hcd.c
> @@ -343,9 +343,11 @@ static void dummy_giveback(struct dummy
> {
> bool fifo = req == &dum->fifo_req;
>
> + ++dum->callback_usage;
> spin_unlock(&dum->lock);
> usb_gadget_giveback_request(_ep, &req->req);
> spin_lock(&dum->lock);
> + --dum->callback_usage;
> if (fifo)
> dum->fifo_req_busy = 0;
> }
> @@ -759,11 +761,13 @@ static int dummy_queue(struct usb_ep *_e
> req->req.complete = fifo_complete;
>
> list_add_tail(&req->queue, &ep->queue);
> + ++dum->callback_usage;
> spin_unlock(&dum->lock);
> _req->actual = _req->length;
> _req->status = 0;
> usb_gadget_giveback_request(_ep, _req);
> spin_lock(&dum->lock);
> + --dum->callback_usage;
> } else
> list_add_tail(&req->queue, &ep->queue);
> spin_unlock_irqrestore(&dum->lock, flags);
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-17 14:02 ` Minseo Kim
@ 2026-09-17 15:38 ` Alan Stern
2026-09-18 13:30 ` Minseo Kim
0 siblings, 1 reply; 45+ messages in thread
From: Alan Stern @ 2026-09-17 15:38 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Thu, Sep 17, 2026 at 11:02:13PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> Thank you for the patch.
>
> > Can you repeat these tests with the patch below applied on top
> > of the other two? It should cause usb_gadget_unregister_driver() to
> > wait until ep_aio_complete() returns (the actual wait loop is in
> > dummy-hcd's dummy_udc_async_callbacks() routine).
>
> Yes. With the new dummy_hcd change applied on top of the two GadgetFS
> patches, usb_gadget_unregister_driver() waited in
> dummy_udc_async_callbacks() until the held ep_aio_complete() callback
> returned in both the normal transfer path through dummy_giveback() and
> the direct giveback path in dummy_queue().
>
> I used upstream v7.2-rc1 as the base. Because dummy_giveback() is not
> present in v7.2-rc1, both matched test trees also included the changes
> from upstream commit d5e5cd3654d2b5359a12ea6586120f05b28634ee
> ("usb: gadget: dummy_hcd: prevent fifo_req reuse during giveback").
Okay. I recently rebased my kernel tree to v7.3-rc3, so a few
discrepancies are to be expected.
> In the normal transfer case, I held ep_aio_complete() immediately after
> iocb->ki_complete(). Without the new dummy_hcd change,
> dummy_udc_async_callbacks(false) saw callback_usage equal to 0, and the
> ep0 close returned while the callback was still held. With the change,
> it saw callback_usage equal to 1, and the ep0 close task remained blocked
> in dummy_udc_async_callbacks(). The relevant part of the blocked close
> task's stack was:
>
> dummy_udc_async_callbacks [dummy_hcd]
> gadget_unbind_driver
> device_remove
> device_release_driver_internal
> driver_detach
> bus_remove_driver
> driver_unregister
> usb_gadget_unregister_driver
> dev_release [gadgetfs]
That is just as it should be.
dummy-hcd emulates a UDC driver. With a real driver, givebacks would be
triggered by a device interrupt (signalling completion of a request) and
the synchronize_irq() call in gadget_unbind_driver() would wait until
outstanding calls to the IRQ handler (and thus the completion handler)
had completed. But dummy-hcd doesn't have real hardware, so instead of
device interrupts it relies on timer interrupts and its
dummy_ucd_async_callbacks() routine is supposed to emulate
synchronize_irq(). Thus it should wait until all outstanding completion
handler calls have completed.
> After I released the callback gate, ep_aio_complete() returned,
> callback_usage fell to 0, and the same close completed. I observed this
> sequence in four separate runs with the new change.
>
> To exercise the other accounting site, I separately tested the direct
> giveback path for a 64-byte write in dummy_queue(). With the change, the
> close task again waited in dummy_udc_async_callbacks() with callback_usage
> equal to 1. In the matched control, dummy_udc_async_callbacks() returned
> with callback_usage at 0, and gadgetfs_unbind() then remained in its
> existing udc_usage wait until I released the callback gate.
I wonder which part of the code was holding udc_usage above 0?
> I also repeated the case where the request was still on the endpoint
> queue when usb_ep_disable() began. Neither usb_ep_disable() nor the ep0
> close returned while the completion callback was held. After I released
> the callback gate, io_getevents() returned one completion with
> res=-ESHUTDOWN, and a subsequent check returned no additional completion
> event.
It all sounds good. I will submit the patches, with your Signed-off-by:
added to this one also, if that's okay with you.
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-17 15:38 ` Alan Stern
@ 2026-09-18 13:30 ` Minseo Kim
2026-09-18 14:17 ` Alan Stern
0 siblings, 1 reply; 45+ messages in thread
From: Minseo Kim @ 2026-09-18 13:30 UTC (permalink / raw)
To: Alan Stern
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
Hi Alan,
> I wonder which part of the code was holding udc_usage above 0?
It was the udc_usage increment in ep_aio() during request submission.
The increment occurs before dev->lock is released to call
kiocb_set_cancel_fn() and usb_ep_queue(), and the corresponding decrement
occurs after usb_ep_queue() returns:
++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;
In the matched control, a request with a length of 64 bytes took the
direct giveback path, where dummy_queue() calls
usb_gadget_giveback_request() before returning to usb_ep_queue().
usb_gadget_giveback_request() synchronously invokes the request's
completion callback, which was ep_aio_complete() in this test. Because I
held ep_aio_complete() at the diagnostic gate immediately after
iocb->ki_complete() returned, the nested giveback call could not return.
Consequently, neither dummy_queue() nor usb_ep_queue() had returned, and
ep_aio() had not reached the decrement. gadgetfs_unbind() therefore saw
udc_usage at 1 and waited.
In the normal transfer test, by contrast, usb_ep_queue() returned and
ep_aio() decremented udc_usage before the later completion callback ran.
I confirmed this ordering in three runs without enabling a callback
gate.
I verified the sequence for direct giveback with markers after the
increment, after usb_ep_queue() returned but before the decrement, after
the decrement, and immediately before and after the unbind wait. In each
of three runs, markers with the same dev pointer showed ep_aio()
incrementing udc_usage to 1 and gadgetfs_unbind() entering the udc_usage
wait with the count still at 1. After I released the diagnostic gate,
usb_ep_queue() returned, ep_aio() decremented the count to 0, and
gadgetfs_unbind() left the wait.
> I will submit the patches, with your Signed-off-by:
> added to this one also, if that's okay with you.
Yes, that is okay with me. Please add it as:
Minseo Kim <neck3922@gmail.com>
Thank you for taking this forward.
With sincere appreciation and great respect,
Minseo Kim
2026년 9월 18일 (금) 오전 12:38, Alan Stern <stern@rowland.harvard.edu>님이 작성:
>
> On Thu, Sep 17, 2026 at 11:02:13PM +0900, Minseo Kim wrote:
> > Hi Alan,
> >
> > Thank you for the patch.
> >
> > > Can you repeat these tests with the patch below applied on top
> > > of the other two? It should cause usb_gadget_unregister_driver() to
> > > wait until ep_aio_complete() returns (the actual wait loop is in
> > > dummy-hcd's dummy_udc_async_callbacks() routine).
> >
> > Yes. With the new dummy_hcd change applied on top of the two GadgetFS
> > patches, usb_gadget_unregister_driver() waited in
> > dummy_udc_async_callbacks() until the held ep_aio_complete() callback
> > returned in both the normal transfer path through dummy_giveback() and
> > the direct giveback path in dummy_queue().
> >
> > I used upstream v7.2-rc1 as the base. Because dummy_giveback() is not
> > present in v7.2-rc1, both matched test trees also included the changes
> > from upstream commit d5e5cd3654d2b5359a12ea6586120f05b28634ee
> > ("usb: gadget: dummy_hcd: prevent fifo_req reuse during giveback").
>
> Okay. I recently rebased my kernel tree to v7.3-rc3, so a few
> discrepancies are to be expected.
>
> > In the normal transfer case, I held ep_aio_complete() immediately after
> > iocb->ki_complete(). Without the new dummy_hcd change,
> > dummy_udc_async_callbacks(false) saw callback_usage equal to 0, and the
> > ep0 close returned while the callback was still held. With the change,
> > it saw callback_usage equal to 1, and the ep0 close task remained blocked
> > in dummy_udc_async_callbacks(). The relevant part of the blocked close
> > task's stack was:
> >
> > dummy_udc_async_callbacks [dummy_hcd]
> > gadget_unbind_driver
> > device_remove
> > device_release_driver_internal
> > driver_detach
> > bus_remove_driver
> > driver_unregister
> > usb_gadget_unregister_driver
> > dev_release [gadgetfs]
>
> That is just as it should be.
>
> dummy-hcd emulates a UDC driver. With a real driver, givebacks would be
> triggered by a device interrupt (signalling completion of a request) and
> the synchronize_irq() call in gadget_unbind_driver() would wait until
> outstanding calls to the IRQ handler (and thus the completion handler)
> had completed. But dummy-hcd doesn't have real hardware, so instead of
> device interrupts it relies on timer interrupts and its
> dummy_ucd_async_callbacks() routine is supposed to emulate
> synchronize_irq(). Thus it should wait until all outstanding completion
> handler calls have completed.
>
> > After I released the callback gate, ep_aio_complete() returned,
> > callback_usage fell to 0, and the same close completed. I observed this
> > sequence in four separate runs with the new change.
> >
> > To exercise the other accounting site, I separately tested the direct
> > giveback path for a 64-byte write in dummy_queue(). With the change, the
> > close task again waited in dummy_udc_async_callbacks() with callback_usage
> > equal to 1. In the matched control, dummy_udc_async_callbacks() returned
> > with callback_usage at 0, and gadgetfs_unbind() then remained in its
> > existing udc_usage wait until I released the callback gate.
>
> I wonder which part of the code was holding udc_usage above 0?
>
> > I also repeated the case where the request was still on the endpoint
> > queue when usb_ep_disable() began. Neither usb_ep_disable() nor the ep0
> > close returned while the completion callback was held. After I released
> > the callback gate, io_getevents() returned one completion with
> > res=-ESHUTDOWN, and a subsequent check returned no additional completion
> > event.
>
> It all sounds good. I will submit the patches, with your Signed-off-by:
> added to this one also, if that's okay with you.
>
> Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel()
2026-09-18 13:30 ` Minseo Kim
@ 2026-09-18 14:17 ` Alan Stern
0 siblings, 0 replies; 45+ messages in thread
From: Alan Stern @ 2026-09-18 14:17 UTC (permalink / raw)
To: Minseo Kim
Cc: Greg Kroah-Hartman, Andrey Konovalov, linux-usb, linux-kernel, syzkaller
On Fri, Sep 18, 2026 at 10:30:00PM +0900, Minseo Kim wrote:
> Hi Alan,
>
> > I wonder which part of the code was holding udc_usage above 0?
>
> It was the udc_usage increment in ep_aio() during request submission.
> The increment occurs before dev->lock is released to call
> kiocb_set_cancel_fn() and usb_ep_queue(), and the corresponding decrement
> occurs after usb_ep_queue() returns:
>
> ++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;
>
> In the matched control, a request with a length of 64 bytes took the
> direct giveback path, where dummy_queue() calls
> usb_gadget_giveback_request() before returning to usb_ep_queue().
> usb_gadget_giveback_request() synchronously invokes the request's
> completion callback, which was ep_aio_complete() in this test. Because I
> held ep_aio_complete() at the diagnostic gate immediately after
> iocb->ki_complete() returned, the nested giveback call could not return.
> Consequently, neither dummy_queue() nor usb_ep_queue() had returned, and
> ep_aio() had not reached the decrement. gadgetfs_unbind() therefore saw
> udc_usage at 1 and waited.
Of course. Now I get it.
> In the normal transfer test, by contrast, usb_ep_queue() returned and
> ep_aio() decremented udc_usage before the later completion callback ran.
> I confirmed this ordering in three runs without enabling a callback
> gate.
>
> I verified the sequence for direct giveback with markers after the
> increment, after usb_ep_queue() returned but before the decrement, after
> the decrement, and immediately before and after the unbind wait. In each
> of three runs, markers with the same dev pointer showed ep_aio()
> incrementing udc_usage to 1 and gadgetfs_unbind() entering the udc_usage
> wait with the count still at 1. After I released the diagnostic gate,
> usb_ep_queue() returned, ep_aio() decremented the count to 0, and
> gadgetfs_unbind() left the wait.
>
> > I will submit the patches, with your Signed-off-by:
> > added to this one also, if that's okay with you.
>
> Yes, that is okay with me. Please add it as:
>
> Minseo Kim <neck3922@gmail.com>
I will, and I will CC: you on the submissions.
Alan Stern
^ permalink raw reply [flat|nested] 45+ messages in thread
end of thread, other threads:[~2026-09-18 14:17 UTC | newest]
Thread overview: 45+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-30 23:08 [BUG] usb: gadgetfs: KASAN null-ptr-deref and intermittent UAF in ep_aio_cancel() 김민서
2026-07-01 2:13 ` Alan Stern
2026-07-02 8:08 ` 김민서
2026-07-02 14:22 ` Alan Stern
2026-07-06 1:18 ` 김민서
2026-07-07 17:31 ` Alan Stern
2026-07-13 18:47 ` Minseo Kim
2026-07-14 3:23 ` Alan Stern
2026-07-19 20:59 ` Minseo Kim
2026-07-21 2:18 ` Alan Stern
2026-07-29 15:31 ` Alan Stern
2026-07-30 15:55 ` Minseo Kim
2026-07-31 18:59 ` Alan Stern
2026-08-02 6:08 ` Minseo Kim
2026-08-02 16:07 ` Alan Stern
2026-08-04 23:12 ` Minseo Kim
2026-08-05 16:17 ` Alan Stern
2026-08-09 22:19 ` Andrey Konovalov
2026-08-14 16:17 ` Alan Stern
2026-08-17 19:49 ` Minseo Kim
2026-08-18 2:57 ` Alan Stern
2026-08-20 9:40 ` Minseo Kim
2026-08-20 14:08 ` Alan Stern
2026-08-22 20:26 ` Minseo Kim
2026-08-23 1:10 ` Alan Stern
2026-08-28 20:30 ` neck3922
2026-08-29 16:08 ` Alan Stern
2026-08-31 13:30 ` Minseo Kim
2026-09-01 2:47 ` Alan Stern
2026-09-04 14:00 ` Minseo Kim
2026-09-04 20:09 ` Alan Stern
2026-09-07 13:00 ` Minseo Kim
2026-09-08 19:02 ` Alan Stern
2026-09-10 14:00 ` Minseo Kim
2026-09-10 15:50 ` Alan Stern
2026-09-11 14:00 ` Minseo Kim
2026-09-11 19:22 ` Alan Stern
2026-09-14 14:00 ` Minseo Kim
2026-09-14 16:00 ` Alan Stern
2026-09-16 14:00 ` Minseo Kim
2026-09-16 19:39 ` Alan Stern
2026-09-17 14:02 ` Minseo Kim
2026-09-17 15:38 ` Alan Stern
2026-09-18 13:30 ` Minseo Kim
2026-09-18 14:17 ` Alan Stern
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®