Re: [PATCH] net/9p/usbg: Fix use-after-free in disable_usb9pfs()
From: XIAO WU
Date: Fri Jun 12 2026 - 16:28:37 EST
Hi Yizhou,
On Sun, Jun 07, 2026 at 09:01:16PM +0800, Yizhou Zhao wrote:
> disable_usb9pfs() frees the IN and OUT usb_request objects before it
> disables the corresponding endpoints. If either request is still queued,
> the later usb_ep_disable() call cancels the endpoint queue and the UDC
> driver can still access the already freed request.
This patch correctly moves disable_ep() before usb_ep_free_request() to
prevent the use-after-free in the endpoint cancellation path.
However, while verifying this patch with KASAN and dummy_hcd, I found a
separate bug in the alloc_requests() error path that still leads to a
kernel panic in usb9pfs_clear_tx() during gadget unbind.
The root cause is that alloc_requests() frees in_req on failure but
does not set usb9pfs->in_req to NULL, leaving a dangling pointer.
Later, when the gadget is unbound via configfs, the call chain
reset_config() -> usb9pfs_clear_tx() dereferences the dangling
in_req->context and crashes:
Oops: general protection fault, probably for non-canonical address
0xdffffc0000000060: 0000 [#1] SMP KASAN NOPTI
KASAN: null-ptr-deref in range [0x0000000000000300-0x0000000000000307]
CPU: 0 UID: 0 PID: 10145 Comm: rm Not tainted 7.1.0-rc6 #1
RIP: 0010:strcmp+0x5b/0xb0
Call Trace:
<TASK>
look_up_lock_class+0x6b/0x130
register_lock_class+0x2cb/0x540
__lock_acquire+0xac/0x2730
lock_acquire+0x1ae/0x360
__wake_up+0x21/0x60
p9_client_cb+0x59/0x80
usb9pfs_clear_tx+0xe1/0x150 <-- dereferences dangling in_req->context
reset_config+0xbe/0x2b0
__composite_disconnect+0xb6/0x160
configfs_composite_disconnect+0xed/0x130
usb_gadget_disconnect_locked+0x214/0x500
gadget_unbind_driver+0xe2/0x520
...
configfs_unlink+0x3f6/0x840
vfs_unlink+0x2f5/0xbd0
Kernel panic - not syncing: Fatal exception
The reproducer:
1. Create a USB gadget with the usb9pfs function
2. Set buflen=0 so that alloc_ep_req() fails inside alloc_requests()
3. Link the function and enable the UDC (enable_usb9pfs() fails)
4. Unbind the gadget (configfs unlink or echo "" > UDC)
I wrote the following PoC to trigger this bug. It creates a USB
gadget with a usb9pfs function, sets buflen=0 so that alloc_ep_req()
fails in alloc_requests(), which frees in_req without NULLing the
pointer, then unbinds the gadget to trigger usb9pfs_clear_tx() on the
dangling in_req.
---8<--- poc.c ---
/*
* PoC: Dangling pointer dereference in usb9pfs_clear_tx()
* via alloc_requests() failure path.
*
* Patch: net/9p/usbg: Fix use-after-free in disable_usb9pfs()
*
* alloc_requests()'s fail_in path frees usb9pfs->in_req without
* NULLing the pointer. Later, when the gadget is unbound,
* usb9pfs_clear_tx() dereferences the dangling pointer.
*
* Trigger:
* 1. Create USB gadget with usb9pfs function
* 2. Set buflen=0 so alloc_ep_req fails -> alloc_requests fails
* -> in_req freed, NOT NULLed (dangling pointer)
* 3. Link function, enable UDC
* 4. Disable UDC -> unbind -> usb9pfs_clear_tx -> CRASH
*
* Build: gcc -Wall -O2 -o poc poc.c
* Run: ./poc (root, KASAN-enabled kernel, dummy_hcd loaded)
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdarg.h>
#include <fcntl.h>
static int do_cmd(const char *fmt, ...)
{
char cmd[1024];
va_list ap;
va_start(ap, fmt);
vsnprintf(cmd, sizeof(cmd), fmt, ap);
va_end(ap);
return system(cmd);
}
int main(void)
{
printf("=== USB9PFS Dangling Pointer PoC ===\n\n");
system("rm -rf /sys/kernel/config/usb_gadget/g1 2>/dev/null");
system("rmdir /sys/kernel/config/usb_gadget/g1 2>/dev/null");
/* Create gadget */
do_cmd("mkdir -p /sys/kernel/config/usb_gadget/g1/configs/c.1/"
"strings/0x409");
do_cmd("mkdir -p /sys/kernel/config/usb_gadget/g1/functions/"
"usb9pfs.gg");
do_cmd("mkdir -p /sys/kernel/config/usb_gadget/g1/strings/0x409");
do_cmd("echo 0x1d6b > "
"/sys/kernel/config/usb_gadget/g1/idVendor");
do_cmd("echo 0x0104 > "
"/sys/kernel/config/usb_gadget/g1/idProduct");
do_cmd("echo 0x0200 > "
"/sys/kernel/config/usb_gadget/g1/bcdUSB");
do_cmd("echo 1234 > /sys/kernel/config/usb_gadget/g1/strings/"
"0x409/serialnumber");
do_cmd("echo test > /sys/kernel/config/usb_gadget/g1/strings/"
"0x409/manufacturer");
do_cmd("echo test > /sys/kernel/config/usb_gadget/g1/strings/"
"0x409/product");
do_cmd("echo Config1 > /sys/kernel/config/usb_gadget/g1/configs/"
"c.1/strings/0x409/configuration");
/* buflen=0 causes alloc_ep_req() -> alloc_requests() failure */
printf("[*] Set buflen=0\n");
do_cmd("echo 0 > /sys/kernel/config/usb_gadget/g1/functions/"
"usb9pfs.gg/buflen");
/* Link function */
printf("[*] Link function\n");
do_cmd("ln -s /sys/kernel/config/usb_gadget/g1/functions/"
"usb9pfs.gg "
"/sys/kernel/config/usb_gadget/g1/configs/c.1/");
/* Enable: in_req freed but not NULLed */
printf("[*] Enable UDC\n");
do_cmd("echo dummy_udc.0 > "
"/sys/kernel/config/usb_gadget/g1/UDC");
sleep(1);
/* Disable: triggers unbind -> usb9pfs_clear_tx -> KASAN */
printf("[*] Disable UDC (expect KASAN report)\n");
do_cmd("echo '' > /sys/kernel/config/usb_gadget/g1/UDC");
sleep(1);
printf("[*] Done. Check dmesg for KASAN null-ptr-deref.\n");
return 0;
}
---8<---
Step 2 triggers the fail_in error path in alloc_requests():
static int alloc_requests(struct f_usb9pfs *usb9pfs)
{
usb9pfs->in_req = usb_ep_alloc_request(usb9pfs->in_ep, GFP_KERNEL);
...
usb9pfs->out_req = alloc_ep_req(usb9pfs->out_ep, usb9pfs->buflen);
if (!usb9pfs->out_req) // buflen=0 causes this to fail
goto fail_in;
...
fail_in:
usb_ep_free_request(usb9pfs->in_ep, usb9pfs->in_req);
// BUG: usb9pfs->in_req is NOT set to NULL here
fail:
return ret;
}
usb9pfs->in_req now points to freed memory. In step 4, the composite
framework calls usb9pfs_disable() -> usb9pfs_clear_tx(), which does:
guard(spinlock_irqsave)(&usb9pfs->lock);
req = usb9pfs->in_req->context; // dangling pointer dereference
This is a regression from a3be076dc174 ("net/9p/usbg: Add new usb gadget
function transport"). The fix is to set usb9pfs->in_req = NULL after
freeing it in the error path:
fail_in:
usb_ep_free_request(usb9pfs->in_ep, usb9pfs->in_req);
+ usb9pfs->in_req = NULL;
fail:
return ret;
A prior review on Sashiko[1] also identified this issue and noted
several other problems in the same file (double-free in the
disable_usb9pfs() path after an alloc_requests failure, missing cleanup
in tx/rx completion error paths, and a potential deadlock in
p9_usbg_request). The alloc_requests error path NULL fix is the
minimum fix needed for the crash reported here.
[1] https://sashiko.dev/#/patchset/20260607130118.16579-1-zhaoyz24%40mails.tsinghua.edu.cn
Hope this is helpful for further fix, thanks.
Best,
Xiao