* [PATCH net] slip: fix use-after-free in sl_sync()
@ 2026-08-17 7:09 Aleksandr Khromov
0 siblings, 0 replies; only message in thread
From: Aleksandr Khromov @ 2026-08-17 7:09 UTC (permalink / raw)
To: Andrew Lunn
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
netdev, linux-kernel, lvc-project, rrv, haa
slip_devs[] stores bare net_device pointers and takes no reference on
them. sl_sync() and sl_alloc() walk that table from slip_open() under
rtnl_lock(), while an entry is dropped by sl_free_netdev(), which
sl_setup() installs as dev->priv_destructor.
priv_destructor is called from netdev_run_todo(), and netdev_run_todo()
deliberately runs with the RTNL semaphore released so that it can sleep
while waiting for the device refcount to drop:
/* Snapshot list, allow later requests */
list_replace_init(&net_todo_list, &list);
__rtnl_unlock();
...
if (dev->priv_destructor)
dev->priv_destructor(dev); /* slip_devs[i] = NULL */
if (dev->needs_free_netdev)
free_netdev(dev);
...
/* Free network device */
kobject_put(&dev->dev.kobj);
So rtnl_lock() does not serialise slip_open() against the teardown at
all. sl_sync() can load slip_devs[i] while the entry is still published
and dereference it after netdev_run_todo() has run the destructor and
released the device:
CPU0 (slip_open) CPU1 (slip_close)
unregister_netdev()
rtnl_unlock()
netdev_run_todo()
__rtnl_unlock()
rtnl_lock()
sl_sync()
dev = slip_devs[i]
priv_destructor(dev)
slip_devs[i] = NULL
kobject_put(&dev->dev.kobj)
/* dev is freed */
sl = netdev_priv(dev)
if (sl->tty || sl->leased) /* use-after-free */
BUG: KASAN: use-after-free in sl_sync drivers/net/slip/slip.c:730 [inline]
BUG: KASAN: use-after-free in slip_open+0xef4/0x1210 drivers/net/slip/slip.c:806
Read of size 1 at addr ffff8880712dac71 by task syz-executor.2/6506
CPU: 2 PID: 6506 Comm: syz-executor.2 Not tainted 6.1.134-syzkaller-00260-g0c8fc3469765 #0
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.12.0-1 04/01/2014
Call Trace:
sl_sync drivers/net/slip/slip.c:730 [inline]
slip_open+0xef4/0x1210 drivers/net/slip/slip.c:806
tty_ldisc_open+0xa2/0x120 drivers/tty/tty_ldisc.c:433
tty_set_ldisc+0x324/0x720 drivers/tty/tty_ldisc.c:564
tiocsetd drivers/tty/tty_io.c:2428 [inline]
tty_ioctl+0x5f0/0x1530 drivers/tty/tty_io.c:2712
Allocated by task 6502:
alloc_netdev_mqs+0x98/0xfe0 net/core/dev.c:10719
sl_alloc drivers/net/slip/slip.c:756 [inline]
slip_open+0x36d/0x1210 drivers/net/slip/slip.c:817
tty_ldisc_open+0xa2/0x120 drivers/tty/tty_ldisc.c:433
tty_set_ldisc+0x324/0x720 drivers/tty/tty_ldisc.c:564
Freed by task 6497:
device_release+0xa2/0x240 drivers/base/core.c:2507
kobject_put+0x179/0x280 lib/kobject.c:729
netdev_run_todo+0x6c8/0xef0 net/core/dev.c:10509
slip_close+0x166/0x1c0 drivers/net/slip/slip.c:906
tty_ldisc_close+0x113/0x1a0 drivers/tty/tty_ldisc.c:456
tty_ldisc_kill+0x94/0x160 drivers/tty/tty_ldisc.c:614
tty_ldisc_release+0xe3/0x2b0 drivers/tty/tty_ldisc.c:782
tty_release+0xbcc/0xe70 drivers/tty/tty_io.c:1860
Commit e58c19124189 ("slip: Fix use-after-free Read in slip_open") fixed
a different source of stale entries - a device left in slip_devs[] after
slip_open() freed it on the registration error path - and does not
address this race, which is why the report survives it.
The same missing exclusion also lets sl_free_netdev() clear a slot that
sl_alloc() has already reused for another channel, silently dropping a
live device from the table. sl_sync() then stops at that NULL entry, and
the next sl_alloc() hands out the same index again, so
register_netdevice() fails with -EEXIST because the slN interface is
still registered.
Give slip_devs[] its own mutex and take it on both sides. A mutex rather
than a spinlock, because sl_sync() calls dev_close() inside the loop.
Holding it across the dereference is enough: the device is released by
the kobject_put() that follows the destructor, so sl_free_netdev() blocks
until sl_sync() is done, and a destructor that already ran leaves NULL
behind for sl_sync() to find. sl_free_netdev() now also clears the slot
only if it still refers to its own device.
Taking rtnl_lock() inside sl_free_netdev() instead would deadlock: the
destructor is also invoked with RTNL already held, both from the error
unwind of register_netdevice() and directly from the error path of
slip_open() itself.
Lock order is rtnl -> slip_devs_lock everywhere; nothing takes RTNL while
holding the new mutex, so there is no inversion.
Reproduced on x86_64 with several threads looping over
open("/dev/ptmx") + ioctl(TIOCSETD, N_SLIP) + close().
Found by Linux Verification Center (linuxtesting.org) with Syzkaller.
Fixes: 5342b77c4123 ("slip: Clean up create and destroy")
Signed-off-by: Aleksandr Khromov <haa@amicon.ru>
---
drivers/net/slip/slip.c | 32 ++++++++++++++++++++++++++++----
1 file changed, 28 insertions(+), 4 deletions(-)
diff --git a/drivers/net/slip/slip.c b/drivers/net/slip/slip.c
index 6865d32..119bb3c 100644
--- a/drivers/net/slip/slip.c
+++ b/drivers/net/slip/slip.c
@@ -96,6 +96,12 @@
static struct net_device **slip_devs;
+/* Serialises access to slip_devs[] and to the slip channels it points at.
+ * RTNL is not enough: sl_free_netdev() is the priv_destructor and therefore
+ * runs from netdev_run_todo(), which deliberately drops the RTNL semaphore.
+ */
+static DEFINE_MUTEX(slip_devs_lock);
+
static int slip_maxdev = SL_NRUNIT;
module_param(slip_maxdev, int, 0);
MODULE_PARM_DESC(slip_maxdev, "Maximum number of slip devices");
@@ -635,7 +641,13 @@ static void sl_free_netdev(struct net_device *dev)
{
int i = dev->base_addr;
- slip_devs[i] = NULL;
+ mutex_lock(&slip_devs_lock);
+ /* Only drop our own entry: the slot may already have been reused by
+ * sl_alloc() for a different channel.
+ */
+ if (slip_devs[i] == dev)
+ slip_devs[i] = NULL;
+ mutex_unlock(&slip_devs_lock);
}
static const struct net_device_ops sl_netdev_ops = {
@@ -721,6 +733,7 @@ static void sl_sync(void)
struct net_device *dev;
struct slip *sl;
+ mutex_lock(&slip_devs_lock);
for (i = 0; i < slip_maxdev; i++) {
dev = slip_devs[i];
if (dev == NULL)
@@ -732,6 +745,7 @@ static void sl_sync(void)
if (dev->flags & IFF_UP)
dev_close(dev);
}
+ mutex_unlock(&slip_devs_lock);
}
@@ -743,19 +757,24 @@ static struct slip *sl_alloc(void)
struct net_device *dev = NULL;
struct slip *sl;
+ mutex_lock(&slip_devs_lock);
for (i = 0; i < slip_maxdev; i++) {
dev = slip_devs[i];
if (dev == NULL)
break;
}
/* Sorry, too many, all slots in use */
- if (i >= slip_maxdev)
+ if (i >= slip_maxdev) {
+ mutex_unlock(&slip_devs_lock);
return NULL;
+ }
sprintf(name, "sl%d", i);
dev = alloc_netdev(sizeof(*sl), name, NET_NAME_UNKNOWN, sl_setup);
- if (!dev)
+ if (!dev) {
+ mutex_unlock(&slip_devs_lock);
return NULL;
+ }
dev->base_addr = i;
sl = netdev_priv(dev);
@@ -772,6 +791,7 @@ static struct slip *sl_alloc(void)
timer_setup(&sl->outfill_timer, sl_outfill, 0);
#endif
slip_devs[i] = dev;
+ mutex_unlock(&slip_devs_lock);
return sl;
}
@@ -1329,6 +1349,7 @@ static void __exit slip_exit(void)
msleep_interruptible(100);
busy = 0;
+ mutex_lock(&slip_devs_lock);
for (i = 0; i < slip_maxdev; i++) {
dev = slip_devs[i];
if (!dev)
@@ -1341,16 +1362,19 @@ static void __exit slip_exit(void)
}
spin_unlock_bh(&sl->lock);
}
+ mutex_unlock(&slip_devs_lock);
} while (busy && time_before(jiffies, timeout));
/* FIXME: hangup is async so we should wait when doing this second
phase */
for (i = 0; i < slip_maxdev; i++) {
+ mutex_lock(&slip_devs_lock);
dev = slip_devs[i];
+ slip_devs[i] = NULL;
+ mutex_unlock(&slip_devs_lock);
if (!dev)
continue;
- slip_devs[i] = NULL;
sl = netdev_priv(dev);
if (sl->tty) {
--
2.48.1
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-08-17 7:25 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-17 7:09 [PATCH net] slip: fix use-after-free in sl_sync() Aleksandr Khromov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox