Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* usb: gadget: aspeed_udc: list iterator used after loop in ast_udc_ep_dequeue
@ 2026-05-17 14:03 Xie Maoyi
  2026-05-17 14:21 ` gregkh
  0 siblings, 1 reply; 4+ messages in thread
From: Xie Maoyi @ 2026-05-17 14:03 UTC (permalink / raw)
  To: neal_liu@aspeedtech.com, gregkh@linuxfoundation.org
  Cc: joel@jms.id.au, andrew@codeconstruct.com.au,
	linux-aspeed@lists.ozlabs.org, linux-usb@vger.kernel.org,
	linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org

[-- Attachment #1: Type: text/plain, Size: 3295 bytes --]

Hi all,

I have been running a small static check for list_for_each_entry
past-the-end patterns, similar to Jakob Koschel's 2022 cleanup
(commit 2966a9918df and related). The check flagged
ast_udc_ep_dequeue() in drivers/usb/gadget/udc/aspeed_udc.c, and I
would like to ask whether you consider this a real defect before I
send anything formal. The same code is present in v7.0 and in
v7.1-rc1 (the two files are byte-identical).

The code in question is around line 691:

    struct ast_udc_request *req;
    ...
    list_for_each_entry(req, &ep->queue, queue) {
        if (&req->req == _req) {
            list_del_init(&req->queue);
            ast_udc_done(ep, req, -ESHUTDOWN);
            _req->status = -ECONNRESET;
            break;
        }
    }
    if (&req->req != _req)
        rc = -EINVAL;

If nothing matches, the loop exits past-the-end and req becomes the
synthetic container_of(&ep->queue, struct ast_udc_request, queue).
Reading &req->req after the loop is undefined per C11. The post-loop
check works in practice only because real _req values do not collide
with that synthetic address.

What made me suspect this was not intentional is that 14 other UDC
drivers in the same directory (at91_udc, atmel_usba_udc, dummy_hcd,
fsl_qe_udc, fsl_udc_core, goku_udc, gr_udc, lpc32xx_udc, max3420_udc,
net2280, omap_udc, pxa25x_udc, pxa27x_udc, udc-xilinx) use a
different pattern, with a separate iter cursor and a result variable.
For example dummy_hcd.c:

    struct dummy_request *req = NULL, *iter;
    list_for_each_entry(iter, &ep->queue, queue) {
        if (&iter->req != _req) continue;
        ...
        req = iter;
        retval = 0;
        break;
    }
    if (retval == 0) { ... }

aspeed_udc seems to be the only outlier in drivers/usb/gadget/udc/,
which is what made me think this was probably an oversight rather
than a deliberate idiom.

I also tried to confirm whether it observably misbehaves. If _req
happens to coincide with the synthetic past-the-end address, the
function returns 0 (success) on an empty queue without removing
anything. I attached a small userspace reproducer (poc_aspeed_udc.c
and its output log) that arranges this collision. In normal use _req
comes from the kernel slab and the collision is unlikely to happen
naturally, so I am not sure whether this rises to the level of a
real bug or just a code-quality issue.

Two questions:

  1. Do you consider the past-the-end use here a defect worth fixing,
     or is it an accepted idiom in this driver that I am misreading?

  2. If it is worth fixing, I already have a small patch that brings
     the function in line with the 14 sibling drivers. Would you like
     me to send it, or would you rather address it locally?

Thanks for taking a look, and apologies if I am off base on any of
this.

Best,
Maoyi Xie
--
Nanyang Technological University
https://maoyixie.com/
________________________________

CONFIDENTIALITY: This email is intended solely for the person(s) named and may be confidential and/or privileged. If you are not the intended recipient, please delete it, notify us and do not copy, use, or disclose its contents.
Towards a sustainable earth: Print only when necessary. Thank you.

[-- Attachment #2: poc_aspeed_udc.log --]
[-- Type: application/octet-stream, Size: 402 bytes --]

$ ./poc_aspeed_udc
[setup] ep.queue=0x7ffefe0b1cd0 (head)
[setup] past_end=0x7ffefe0b1cc0
[setup] fake_req=0x7ffefe0b1cc0
[probe] existing rc=0
[result] returned 0 (success) on empty queue without removing anything

$ ./poc_aspeed_udc patched
[setup] ep.queue=0x7ffee648eee0 (head)
[setup] past_end=0x7ffee648eed0
[setup] fake_req=0x7ffee648eed0
[probe] patched rc=-22
[result] returned -22 (rejected)

[-- Attachment #3: poc_aspeed_udc.c --]
[-- Type: text/plain, Size: 4521 bytes --]

/*
 * Userspace reproducer for the past-the-end iterator behavior in
 * ast_udc_ep_dequeue() (drivers/usb/gadget/udc/aspeed_udc.c).
 *
 * Aspeed UDC is BMC/ARM hardware. Rather than bringing up a full SoC
 * emulation, this program extracts the dequeue function's logic into
 * userspace using mock structs whose layout (req at offset 0, queue
 * immediately after) matches the kernel definition. It then runs both
 * the existing code path and the proposed fix on the same crafted input.
 *
 * Build: cc -O0 -g poc_aspeed_udc.c -o poc_aspeed_udc
 * Run:   ./poc_aspeed_udc           (existing code, returns 42)
 *        ./poc_aspeed_udc patched   (proposed fix, returns 0)
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>

/* Minimal mock of the kernel list_head and container_of. */
struct list_head { struct list_head *next, *prev; };

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

#define list_first_entry(ptr, type, member) \
    container_of((ptr)->next, type, member)
#define list_next_entry(pos, member) \
    container_of((pos)->member.next, typeof(*(pos)), member)
#define list_entry_is_head(pos, head, member) \
    (&(pos)->member == (head))

#define list_for_each_entry(pos, head, member)               \
    for (pos = list_first_entry(head, typeof(*pos), member); \
         !list_entry_is_head(pos, head, member);             \
         pos = list_next_entry(pos, member))

static void list_init(struct list_head *h) { h->next = h->prev = h; }

/* Mock structs. Only field order matters: req at offset 0, queue
 * immediately after. */
struct usb_request {
    void *buf;
    unsigned length;
    int status;
};

struct ast_udc_request {
    struct usb_request req;
    struct list_head queue;
    int pad;
};

struct ast_udc_ep {
    struct list_head queue;
};

/* Existing code path from aspeed_udc.c around line 691. Locks and
 * the ast_udc_done() callback are elided since the past-the-end
 * behavior is independent of them. */
static int ast_udc_ep_dequeue_existing(struct ast_udc_ep *ep,
                                        struct usb_request *_req)
{
    struct ast_udc_request *req;
    int rc = 0;

    list_for_each_entry(req, &ep->queue, queue) {
        if (&req->req == _req) {
            /* list_del_init + ast_udc_done + set status here */
            break;
        }
    }

    /* When the loop finds no match, req is past-the-end. Reading
     * &req->req is undefined per C11; the resulting check is a
     * property of heap layout rather than the queue contents. */
    if (&req->req != _req)
        rc = -22;     /* -EINVAL */

    return rc;
}

/* Proposed fix using the separate iter cursor pattern shared by the
 * other UDC drivers in the same directory (e.g. dummy_hcd.c). */
static int ast_udc_ep_dequeue_patched(struct ast_udc_ep *ep,
                                       struct usb_request *_req)
{
    struct ast_udc_request *req = NULL, *iter;

    list_for_each_entry(iter, &ep->queue, queue) {
        if (&iter->req != _req)
            continue;
        req = iter;
        break;
    }

    if (!req)
        return -22;     /* -EINVAL */

    /* list_del_init + ast_udc_done + set status here */
    return 0;
}

int main(int argc, char **argv)
{
    int use_patched = (argc > 1 && !strcmp(argv[1], "patched"));

    struct ast_udc_ep ep;
    list_init(&ep.queue);

    /* An empty queue forces the existing code's iterator past the end.
     * past_end is the synthetic ast_udc_request pointer the loop will
     * leave behind. Because req is the first member, &past_end->req
     * has the same numeric value as past_end itself. */
    struct ast_udc_request *past_end =
        container_of(&ep.queue, struct ast_udc_request, queue);
    struct usb_request *fake_req = &past_end->req;

    printf("[setup] ep.queue=%p (head)\n", (void *)&ep.queue);
    printf("[setup] past_end=%p\n", (void *)past_end);
    printf("[setup] fake_req=%p\n", (void *)fake_req);

    int rc;
    if (use_patched) {
        rc = ast_udc_ep_dequeue_patched(&ep, fake_req);
        printf("[probe] patched rc=%d\n", rc);
    } else {
        rc = ast_udc_ep_dequeue_existing(&ep, fake_req);
        printf("[probe] existing rc=%d\n", rc);
    }

    if (rc == 0) {
        printf("[result] returned 0 (success) on empty queue without "
               "removing anything\n");
        return 42;
    }
    printf("[result] returned %d (rejected)\n", rc);
    return 0;
}

^ permalink raw reply	[flat|nested] 4+ messages in thread
* usb: gadget: aspeed_udc: list iterator used after loop in ast_udc_ep_dequeue
@ 2026-05-17 15:46 Maoyi Xie
  2026-05-18  2:08 ` Andrew Jeffery
  0 siblings, 1 reply; 4+ messages in thread
From: Maoyi Xie @ 2026-05-17 15:46 UTC (permalink / raw)
  To: neal_liu, gregkh
  Cc: joel, andrew, linux-aspeed, linux-usb, linux-arm-kernel,
	linux-kernel

[-- Attachment #1: Type: text/plain, Size: 3173 bytes --]

Hi all,

(Resending from a personal address — my previous attempt from
my NTU corporate account carried an auto-appended confidentiality
disclaimer that you've declined to accept. The content below is
unchanged.)

I have been running a small static check for list_for_each_entry
past-the-end patterns, similar to Jakob Koschel's 2022 cleanup
(commit 2966a9918df and related). The check flagged
ast_udc_ep_dequeue() in drivers/usb/gadget/udc/aspeed_udc.c, and I
would like to ask whether you consider this a real defect before I
send anything formal. The same code is present in v7.0 and in
v7.1-rc1 (the two files are byte-identical).

The code in question is around line 691:

    struct ast_udc_request *req;
    ...
    list_for_each_entry(req, &ep->queue, queue) {
        if (&req->req == _req) {
            list_del_init(&req->queue);
            ast_udc_done(ep, req, -ESHUTDOWN);
            _req->status = -ECONNRESET;
            break;
        }
    }
    if (&req->req != _req)
        rc = -EINVAL;

If nothing matches, the loop exits past-the-end and req becomes the
synthetic container_of(&ep->queue, struct ast_udc_request, queue).
Reading &req->req after the loop is undefined per C11. The post-loop
check works in practice only because real _req values do not collide
with that synthetic address.

What made me suspect this was not intentional is that 14 other UDC
drivers in the same directory (at91_udc, atmel_usba_udc, dummy_hcd,
fsl_qe_udc, fsl_udc_core, goku_udc, gr_udc, lpc32xx_udc, max3420_udc,
net2280, omap_udc, pxa25x_udc, pxa27x_udc, udc-xilinx) use a
different pattern, with a separate iter cursor and a result variable.
For example dummy_hcd.c:

    struct dummy_request *req = NULL, *iter;
    list_for_each_entry(iter, &ep->queue, queue) {
        if (&iter->req != _req) continue;
        ...
        req = iter;
        retval = 0;
        break;
    }
    if (retval == 0) { ... }

aspeed_udc seems to be the only outlier in drivers/usb/gadget/udc/,
which is what made me think this was probably an oversight rather
than a deliberate idiom.

I also tried to confirm whether it observably misbehaves. If _req
happens to coincide with the synthetic past-the-end address, the
function returns 0 (success) on an empty queue without removing
anything. I attached a small userspace reproducer (poc_aspeed_udc.c
and its output log) that arranges this collision. In normal use _req
comes from the kernel slab and the collision is unlikely to happen
naturally, so I am not sure whether this rises to the level of a
real bug or just a code-quality issue.

Two questions:

  1. Do you consider the past-the-end use here a defect worth fixing,
     or is it an accepted idiom in this driver that I am misreading?

  2. If it is worth fixing, I already have a small patch that brings
     the function in line with the 14 sibling drivers. Would you like
     me to send it, or would you rather address it locally?

Thanks for taking a look, and apologies if I am off base on any of
this.

Best,
Maoyi Xie
--
Nanyang Technological University
https://maoyixie.com/

[-- Attachment #2: poc_aspeed_udc.c --]
[-- Type: application/octet-stream, Size: 4521 bytes --]

/*
 * Userspace reproducer for the past-the-end iterator behavior in
 * ast_udc_ep_dequeue() (drivers/usb/gadget/udc/aspeed_udc.c).
 *
 * Aspeed UDC is BMC/ARM hardware. Rather than bringing up a full SoC
 * emulation, this program extracts the dequeue function's logic into
 * userspace using mock structs whose layout (req at offset 0, queue
 * immediately after) matches the kernel definition. It then runs both
 * the existing code path and the proposed fix on the same crafted input.
 *
 * Build: cc -O0 -g poc_aspeed_udc.c -o poc_aspeed_udc
 * Run:   ./poc_aspeed_udc           (existing code, returns 42)
 *        ./poc_aspeed_udc patched   (proposed fix, returns 0)
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>

/* Minimal mock of the kernel list_head and container_of. */
struct list_head { struct list_head *next, *prev; };

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

#define list_first_entry(ptr, type, member) \
    container_of((ptr)->next, type, member)
#define list_next_entry(pos, member) \
    container_of((pos)->member.next, typeof(*(pos)), member)
#define list_entry_is_head(pos, head, member) \
    (&(pos)->member == (head))

#define list_for_each_entry(pos, head, member)               \
    for (pos = list_first_entry(head, typeof(*pos), member); \
         !list_entry_is_head(pos, head, member);             \
         pos = list_next_entry(pos, member))

static void list_init(struct list_head *h) { h->next = h->prev = h; }

/* Mock structs. Only field order matters: req at offset 0, queue
 * immediately after. */
struct usb_request {
    void *buf;
    unsigned length;
    int status;
};

struct ast_udc_request {
    struct usb_request req;
    struct list_head queue;
    int pad;
};

struct ast_udc_ep {
    struct list_head queue;
};

/* Existing code path from aspeed_udc.c around line 691. Locks and
 * the ast_udc_done() callback are elided since the past-the-end
 * behavior is independent of them. */
static int ast_udc_ep_dequeue_existing(struct ast_udc_ep *ep,
                                        struct usb_request *_req)
{
    struct ast_udc_request *req;
    int rc = 0;

    list_for_each_entry(req, &ep->queue, queue) {
        if (&req->req == _req) {
            /* list_del_init + ast_udc_done + set status here */
            break;
        }
    }

    /* When the loop finds no match, req is past-the-end. Reading
     * &req->req is undefined per C11; the resulting check is a
     * property of heap layout rather than the queue contents. */
    if (&req->req != _req)
        rc = -22;     /* -EINVAL */

    return rc;
}

/* Proposed fix using the separate iter cursor pattern shared by the
 * other UDC drivers in the same directory (e.g. dummy_hcd.c). */
static int ast_udc_ep_dequeue_patched(struct ast_udc_ep *ep,
                                       struct usb_request *_req)
{
    struct ast_udc_request *req = NULL, *iter;

    list_for_each_entry(iter, &ep->queue, queue) {
        if (&iter->req != _req)
            continue;
        req = iter;
        break;
    }

    if (!req)
        return -22;     /* -EINVAL */

    /* list_del_init + ast_udc_done + set status here */
    return 0;
}

int main(int argc, char **argv)
{
    int use_patched = (argc > 1 && !strcmp(argv[1], "patched"));

    struct ast_udc_ep ep;
    list_init(&ep.queue);

    /* An empty queue forces the existing code's iterator past the end.
     * past_end is the synthetic ast_udc_request pointer the loop will
     * leave behind. Because req is the first member, &past_end->req
     * has the same numeric value as past_end itself. */
    struct ast_udc_request *past_end =
        container_of(&ep.queue, struct ast_udc_request, queue);
    struct usb_request *fake_req = &past_end->req;

    printf("[setup] ep.queue=%p (head)\n", (void *)&ep.queue);
    printf("[setup] past_end=%p\n", (void *)past_end);
    printf("[setup] fake_req=%p\n", (void *)fake_req);

    int rc;
    if (use_patched) {
        rc = ast_udc_ep_dequeue_patched(&ep, fake_req);
        printf("[probe] patched rc=%d\n", rc);
    } else {
        rc = ast_udc_ep_dequeue_existing(&ep, fake_req);
        printf("[probe] existing rc=%d\n", rc);
    }

    if (rc == 0) {
        printf("[result] returned 0 (success) on empty queue without "
               "removing anything\n");
        return 42;
    }
    printf("[result] returned %d (rejected)\n", rc);
    return 0;
}

[-- Attachment #3: poc_aspeed_udc.log --]
[-- Type: application/octet-stream, Size: 402 bytes --]

$ ./poc_aspeed_udc
[setup] ep.queue=0x7ffefe0b1cd0 (head)
[setup] past_end=0x7ffefe0b1cc0
[setup] fake_req=0x7ffefe0b1cc0
[probe] existing rc=0
[result] returned 0 (success) on empty queue without removing anything

$ ./poc_aspeed_udc patched
[setup] ep.queue=0x7ffee648eee0 (head)
[setup] past_end=0x7ffee648eed0
[setup] fake_req=0x7ffee648eed0
[probe] patched rc=-22
[result] returned -22 (rejected)

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-05-18  2:08 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-17 14:03 usb: gadget: aspeed_udc: list iterator used after loop in ast_udc_ep_dequeue Xie Maoyi
2026-05-17 14:21 ` gregkh
  -- strict thread matches above, loose matches on Subject: below --
2026-05-17 15:46 Maoyi Xie
2026-05-18  2:08 ` Andrew Jeffery

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox