[PATCH] tty: rpmsg: close port lookup-to-get race

From: Sang-Hoon Choi

Date: Tue Sep 22 2026 - 14:00:59 EST


rpmsg_tty_install() obtains cport from tty_idr before taking a port
reference. rpmsg_tty_destruct_port() removes the entry under idr_lock
and frees cport. If channel removal drops the last reference between
idr_find() and tty_port_get(), the install path dereferences freed
memory.

The first-open path and channel removal can run concurrently. tty_mutex
serializes TTY initialization, but rpmsg_tty_remove() does not take that
mutex. tty_unregister_device() prevents later opens through cdev_del(),
but cdev_del() does not wait for an open which has already entered the
driver. Before rpmsg_tty_install() finishes, the port is not attached to
the new TTY, so tty_port_tty_hangup() does not close this interval.

RPMsg channel removal may be initiated asynchronously by the remote
processor or transport. In the test, the local process only needs
permission to open the TTY node; channel removal is initiated
independently.

I reproduced this with a UML kernel built with KASAN and a synthetic
RPMsg device using the real rpmsg_tty probe and remove paths. Test-only
synchronization forces removal after idr_find() and before
tty_port_get(). The opening process drops to UID 1000 and GID 1000 first.
The unpatched kernel reports:

BUG: KASAN: slab-use-after-free in rpmsg_tty_install
Read of size 4 ... by task init/23
CPU: 0 UID: 1000 PID: 23

The allocation stack ends in rpmsg_tty_probe(). The free stack is
rpmsg_tty_remove() -> tty_port_put() -> rpmsg_tty_destruct_port(), and
the invalid read is in rpmsg_tty_install().

Take idr_lock across idr_find() and tty_port_get(). If the entry is gone
or its reference count has reached zero, fail the installation with
-ENODEV. With the same forced overlap, the UID 1000 open returns ENODEV
and KASAN stays quiet.

The test uses a synthetic transport and deliberately widens the race
window. It demonstrates the lifetime bug and an unprivileged opener, but
does not show that an unprivileged user can deliberately cause channel
removal on every RPMsg platform. I am therefore reporting this as a
normal lifetime bug.

Fixes: 7c0408d80579 ("tty: add rpmsg driver")
Reported-by: Changyul Lee <lcy8047@xxxxxxxxx>
Link: https://lore.kernel.org/all/179000811428.1227592.8003229121862460039.idr-bug-84@xxxxxxxxx/
Assisted-by: LLM
Signed-off-by: Sang-Hoon Choi <csh0052@xxxxxxxxx>
---
Greg, thanks for the feedback. I went back and reproduced the race under
KASAN before preparing this patch. I can also provide the test-only
instrumentation and the complete KASAN log if useful.

drivers/tty/rpmsg_tty.c | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/drivers/tty/rpmsg_tty.c b/drivers/tty/rpmsg_tty.c
index c5fd6d9b3..b2765bae2 100644
--- a/drivers/tty/rpmsg_tty.c
+++ b/drivers/tty/rpmsg_tty.c
@@ -49,12 +49,19 @@ static int rpmsg_tty_cb(struct rpmsg_device *rpdev, void *data, int len, void *p

static int rpmsg_tty_install(struct tty_driver *driver, struct tty_struct *tty)
{
- struct rpmsg_tty_port *cport = idr_find(&tty_idr, tty->index);
- struct tty_port *port;
+ struct rpmsg_tty_port *cport;
+ struct tty_port *port = NULL;

- tty->driver_data = cport;
+ mutex_lock(&idr_lock);
+ cport = idr_find(&tty_idr, tty->index);
+ if (cport)
+ port = tty_port_get(&cport->port);
+ mutex_unlock(&idr_lock);
+
+ if (!port)
+ return -ENODEV;

- port = tty_port_get(&cport->port);
+ tty->driver_data = cport;
return tty_port_install(port, driver, tty);
}

--
2.43.0