* [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®