[PATCH RFC] usb: gadget: u_serial: fix use-after-free of tty->port

"syzbot" <[email protected]>
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
A use-after-free of `tty->port` in `release_tty()` occurs because
`u_serial.c` manually manages the lifetime of `struct gs_port` and frees it
prematurely while the TTY core is still tearing down the `tty_struct`.

When the USB gadget is removed (e.g., via configfs `rmdir`),
`gserial_free_port()` is called. If the TTY device is currently open by
userspace, `gserial_free_port()` blocks and waits for the TTY to be closed.
When userspace closes the last file descriptor for the TTY, the VFS calls
`tty_release()`. `tty_release()` first invokes the driver's
`tty->ops->close()` callback, which is `gs_close()`. In `gs_close()`,
setting `port->port.count = 0` and waking up the waitqueue causes
`gserial_free_port()` to immediately unblock, call `tty_port_destroy()`,
and `kfree(port)`. However, the TTY core is not done yet. After
`gs_close()` returns, `tty_release()` continues its execution and
eventually calls `tty_release_struct()` -> `release_tty()`. Because
`tty->port` points to `&port->port` (which was just freed by
`gserial_free_port()`), this results in a KASAN slab-use-after-free crash.

BUG: KASAN: slab-use-after-free in release_tty+0x36b/0x560
drivers/tty/tty_io.c:1584
Write of size 8 at addr ffff888191b2d120 by task syz.0.17/6133
Call Trace:
 release_tty+0x36b/0x560 drivers/tty/tty_io.c:1584
 tty_release_struct+0xb8/0xd0 drivers/tty/tty_io.c:1692
 tty_release+0xc6b/0x1680 drivers/tty/tty_io.c:1852
 __fput+0x42a/0xa80 fs/file_table.c:512
Allocated by task 6134:
 gs_port_alloc drivers/usb/gadget/function/u_serial.c:1218 [inline]
 gserial_alloc_line_no_console+0x23a/0x6e0
 drivers/usb/gadget/function/u_serial.c:1298
 gserial_alloc_line+0x18/0x90 drivers/usb/gadget/function/u_serial.c:1332
 acm_alloc_instance+0xc7/0x140 drivers/usb/gadget/function/f_acm.c:891
Freed by task 6134:
 gserial_free_port+0x248/0x2c0 drivers/usb/gadget/function/u_serial.c:1262
 gserial_free_line+0xc3/0x1f0 drivers/usb/gadget/function/u_serial.c:1279
 acm_free_instance+0x39/0x60 drivers/usb/gadget/function/f_acm.c:875

To fix this properly, convert `u_serial.c` to use standard `tty_port`
refcounting. This also fixes a secondary issue where `rmdir` hangs
indefinitely if userspace keeps the TTY open.

Implement `tty_port` refcounting by adding `tty_port_operations` with a
`.destruct` callback that handles `cancel_delayed_work_sync(&port->push)`
and `kfree(port)`. Implement `.install` and `.cleanup` in `gs_tty_ops`.
`gs_install()` calls `tty_port_get()` to take a reference, and
`gs_cleanup()` calls `tty_port_put()` to drop it. Change
`gserial_free_port()` to simply call `tty_port_put(&port->port)` instead of
waiting on `close_wait` and freeing the port directly.

Because `rmdir` will no longer block, the USB gadget can be removed while
the TTY is still open. If the port number (`port_num`) is immediately freed
and reused by a new gadget, `tty_open()` will look up the index and
erroneously return the old `tty_struct` (since it hasn't been removed from
the driver's table yet), leading to severe state corruption. To prevent
this, defer setting `ports[port_num].port = NULL` until the `.destruct`
callback. Add a `bool freed;` flag to `struct gs_port`. Set `port->freed =
true` in `gserial_free_line()`, and make `gs_install()` and `gs_open()`
return `-ENODEV` if `port->freed` is true, preventing any new opens on the
dying port.

Finally, handle the case where `gs_close()` is called after a failed
`gs_open()`. Since `gs_open()` didn't increment `port->port.count`,
`gs_close()` should simply return without decrementing the count or
executing the rest of the close logic, avoiding a `WARN_ON(1)` when
`port->port.count == 0`.

Fixes: 19b10a8828a6 ("usb: gadget: allocate & giveback serial ports instead hard code them")
Assisted-by: Gemini:gemini-3.6-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: [email protected]
Closes: https://syzkaller.appspot.com/bug?extid=bca09f5d8b843bbf7571
Link: https://syzkaller.appspot.com/ai_job?id=92d0161e-2ce3-4d72-aa77-1356fd886e84
To: "Greg Kroah-Hartman" <[email protected]>
To: <[email protected]>
To: "Sebastian Andrzej Siewior" <[email protected]>
Cc: "Ai Chao" <[email protected]>
Cc: "Kees Cook" <[email protected]>
Cc: <[email protected]>

---
diff --git a/drivers/usb/gadget/function/u_serial.c b/drivers/usb/gadget/function/u_serial.c
index cdd1dfc66..013044c0d 100644
--- a/drivers/usb/gadget/function/u_serial.c
+++ b/drivers/usb/gadget/function/u_serial.c
@@ -125,10 +125,10 @@ struct gs_port {
 	struct kfifo		port_write_buf;
 	wait_queue_head_t	drain_wait;	/* wait while writes drain */
 	bool                    write_busy;
-	wait_queue_head_t	close_wait;
 	bool			suspended;	/* port suspended */
 	bool			start_delayed;	/* delay start when suspended */
 	struct async_icount	icount;
+	bool			freed;
 
 	/* REVISIT this state ... */
 	struct usb_cdc_line_coding port_line_coding;	/* 8-N-1 etc */
@@ -318,9 +318,10 @@ __acquires(&port->port_lock)
 		struct tty_struct	*tty;
 
 		/* no more rx if closed */
-		tty = port->port.tty;
+		tty = tty_port_tty_get(&port->port);
 		if (!tty)
 			break;
+		tty_kref_put(tty);
 
 		if (port->read_started >= QUEUE_SIZE)
 			break;
@@ -372,7 +373,7 @@ static void gs_rx_push(struct work_struct *work)
 
 	/* hand any queued data to the tty */
 	spin_lock_irq(&port->port_lock);
-	tty = port->port.tty;
+	tty = tty_port_tty_get(&port->port);
 	while (!list_empty(queue)) {
 		struct usb_request	*req;
 
@@ -453,6 +454,9 @@ static void gs_rx_push(struct work_struct *work)
 		gs_start_rx(port);
 
 	spin_unlock_irq(&port->port_lock);
+
+	if (tty)
+		tty_kref_put(tty);
 }
 
 static void gs_read_complete(struct usb_ep *ep, struct usb_request *req)
@@ -611,12 +615,11 @@ static int gserial_wakeup_host(struct gserial *gser)
 static int gs_open(struct tty_struct *tty, struct file *file)
 {
 	int		port_num = tty->index;
-	struct gs_port	*port;
+	struct gs_port	*port = tty->driver_data;
 	int		status = 0;
 
 	mutex_lock(&ports[port_num].lock);
-	port = ports[port_num].port;
-	if (!port) {
+	if (!port || port->freed) {
 		status = -ENODEV;
 		goto out;
 	}
@@ -648,8 +651,7 @@ static int gs_open(struct tty_struct *tty, struct file *file)
 	if (port->port.count++)
 		goto exit_unlock_port;
 
-	tty->driver_data = port;
-	port->port.tty = tty;
+	tty_port_tty_set(&port->port, tty);
 
 	/* if connected, start the I/O stream */
 	if (port->port_usb) {
@@ -700,9 +702,8 @@ static void gs_close(struct tty_struct *tty, struct file *file)
 	if (port->port.count != 1) {
 raced_with_open:
 		if (port->port.count == 0)
-			WARN_ON(1);
-		else
-			--port->port.count;
+			goto exit;
+		--port->port.count;
 		goto exit;
 	}
 
@@ -739,12 +740,11 @@ static void gs_close(struct tty_struct *tty, struct file *file)
 
 	port->start_delayed = false;
 	port->port.count = 0;
-	port->port.tty = NULL;
+	tty_port_tty_set(&port->port, NULL);
 
 	pr_debug("gs_close: ttyGS%d (%p,%p) done!\n",
 			port->port_num, tty, file);
 
-	wake_up(&port->close_wait);
 exit:
 	spin_unlock_irq(&port->port_lock);
 }
@@ -908,9 +908,38 @@ static int gs_get_icount(struct tty_struct *tty,
 	return 0;
 }
 
+static int gs_install(struct tty_driver *driver, struct tty_struct *tty)
+{
+	int		port_num = tty->index;
+	struct gs_port	*port;
+	int		status = -ENODEV;
+
+	mutex_lock(&ports[port_num].lock);
+	port = ports[port_num].port;
+	if (port && !port->freed) {
+		tty_port_get(&port->port);
+		status = tty_port_install(&port->port, driver, tty);
+		if (status)
+			tty_port_put(&port->port);
+		else
+			tty->driver_data = port;
+	}
+	mutex_unlock(&ports[port_num].lock);
+	return status;
+}
+
+static void gs_cleanup(struct tty_struct *tty)
+{
+	struct gs_port	*port = tty->driver_data;
+
+	tty_port_put(&port->port);
+}
+
 static const struct tty_operations gs_tty_ops = {
+	.install =		gs_install,
 	.open =			gs_open,
 	.close =		gs_close,
+	.cleanup =		gs_cleanup,
 	.write =		gs_write,
 	.put_char =		gs_put_char,
 	.flush_chars =		gs_flush_chars,
@@ -1203,6 +1232,24 @@ static void gs_console_exit(struct gs_port *port)
 
 #endif
 
+static void gs_port_destruct(struct tty_port *port)
+{
+	struct gs_port	*p = container_of(port, struct gs_port, port);
+
+	cancel_delayed_work_sync(&p->push);
+
+	mutex_lock(&ports[p->port_num].lock);
+	ports[p->port_num].port = NULL;
+	mutex_unlock(&ports[p->port_num].lock);
+
+	kfifo_free(&p->port_write_buf);
+	kfree(p);
+}
+
+static const struct tty_port_operations gs_port_ops = {
+	.destruct =		gs_port_destruct,
+};
+
 static int
 gs_port_alloc(unsigned port_num, struct usb_cdc_line_coding *coding)
 {
@@ -1222,9 +1269,9 @@ gs_port_alloc(unsigned port_num, struct usb_cdc_line_coding *coding)
 	}
 
 	tty_port_init(&port->port);
+	port->port.ops = &gs_port_ops;
 	spin_lock_init(&port->port_lock);
 	init_waitqueue_head(&port->drain_wait);
-	init_waitqueue_head(&port->close_wait);
 
 	INIT_DELAYED_WORK(&port->push, gs_rx_push);
 
@@ -1241,25 +1288,10 @@ gs_port_alloc(unsigned port_num, struct usb_cdc_line_coding *coding)
 	return ret;
 }
 
-static int gs_closed(struct gs_port *port)
-{
-	int cond;
-
-	spin_lock_irq(&port->port_lock);
-	cond = port->port.count == 0;
-	spin_unlock_irq(&port->port_lock);
-
-	return cond;
-}
-
 static void gserial_free_port(struct gs_port *port)
 {
-	cancel_delayed_work_sync(&port->push);
-	/* wait for old opens to finish */
-	wait_event(port->close_wait, gs_closed(port));
 	WARN_ON(port->port_usb != NULL);
-	tty_port_destroy(&port->port);
-	kfree(port);
+	tty_port_put(&port->port);
 }
 
 void gserial_free_line(unsigned char port_num)
@@ -1272,12 +1304,14 @@ void gserial_free_line(unsigned char port_num)
 		return;
 	}
 	port = ports[port_num].port;
-	gs_console_exit(port);
-	ports[port_num].port = NULL;
+	port->freed = true;
 	mutex_unlock(&ports[port_num].lock);
 
-	gserial_free_port(port);
+	gs_console_exit(port);
+
 	tty_unregister_device(gs_tty_driver, port_num);
+	tty_port_tty_hangup(&port->port, false);
+	gserial_free_port(port);
 }
 EXPORT_SYMBOL_GPL(gserial_free_line);
 
@@ -1457,8 +1491,7 @@ void gserial_disconnect(struct gserial *gser)
 	gser->ioport = NULL;
 	if (port->port.count > 0) {
 		wake_up_interruptible(&port->drain_wait);
-		if (port->port.tty)
-			tty_hangup(port->port.tty);
+		tty_port_tty_hangup(&port->port, false);
 	}
 	port->suspended = false;
 	spin_unlock(&port->port_lock);


base-commit: 8d3ae59288f1e7d58d76558a6ee96d533bc5019f
-- 
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).

See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
You can comment on the patch as usual, syzbot will try to address
the comments and send a new version of the patch if necessary.
syzbot engineers can be reached at [email protected].
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.