[PATCH net v2] net/ncsi: fix use-after-free of ncsi_package in ncsi_remove_package()

From: Binbin Deng

Date: Sun Oct 18 2026 - 21:38:17 EST


ncsi_remove_package() unlinks the package from ndp->packages with
list_del_rcu() and then frees it with kfree(), after having released
all of its channels. The generic netlink package info walkers iterate
the same list with the list_for_each_entry_rcu() based
NCSI_FOR_EACH_PACKAGE() and dereference the package in the loop body:

rc = nla_put_u32(skb, NCSI_PKG_ATTR_ID, np->id);

None of the NCSI genl handlers enters an RCU read-side critical
section: genl doit and dumpit callbacks run under genl_mutex and
cb_mutex, not under rcu_read_lock(), and ncsi_unregister_dev() takes
neither. A reader that is preempted mid-walk -- or one that has
resolved the device and then sleeps in the genlmsg_new(GFP_KERNEL)
allocation of ncsi_pkg_info_nl() -- has no protection at all when the
teardown frees the package under it.

The race needs no real NC-SI hardware and is reachable by an
unprivileged user, as NCSI_CMD_PKG_INFO has no GENL_ADMIN_PERM:

writer: rmmod ftgmac100
-> ftgmac100_remove() -> ncsi_unregister_dev()
-> ncsi_remove_package() -> kfree(np)
reader: sendmsg(NETLINK_GENERIC, NCSI_CMD_PKG_INFO)
-> ncsi_pkg_info_nl() -> ncsi_write_package_info()

ftgmac100_remove() calls ncsi_unregister_dev() before
unregister_netdev(), so the netdev, and with it the genl entry point,
stays reachable during the whole window.

KASAN report (arm multi_v7, QEMU ast2600-evb, KASAN + SLUB + PREEMPT,
four threads looping PKG_INFO doit/dump while cycling insmod/rmmod):

BUG: KASAN: slab-use-after-free in ncsi_write_package_info+0xe3c/0xf84
Read of size 1 at addr c69e2580 by task poc_dump/69
The buggy address belongs to the object at c69e2580
which belongs to the cache kmalloc-64 of size 64
The buggy address is located 0 bytes inside of
freed 64-byte region [c69e2580, c69e25c0)

The read is the u8 np->id feeding the nla_put_u32() above;
sizeof(struct ncsi_package) is 60 bytes, so it lands in kmalloc-64,
and with no VLAN churn in this mode the only size-1 read at offset 0
of that cache on this path is np->id.

Free the package with kfree_rcu() so that it survives the grace
period, and give struct ncsi_package an rcu_head.

This is the free side of the fix for the package object. The read
side -- rcu_read_lock() around the device resolution and the list
walks of all NCSI genl handlers -- is the companion series "net/ncsi:
fix use-after-free of ncsi_channel in ncsi_remove_channel()"; the
parent ncsi_dev_priv is deferred by the companion series "net/ncsi:
fix use-after-free of ncsi_dev_priv in ncsi_unregister_dev()". The
race is closed by the three series together; each one keeps its own
Fixes: tag so they can be backported independently.

Found by a Coccinelle rule matching kfree() immediately after
list_del_rcu(); the code inspection and the fix were LLM-assisted,
and the race was confirmed at runtime with KASAN as above.

Fixes: 2d283bdd079c ("net/ncsi: Resource management")
Signed-off-by: Binbin Deng <18983559317@xxxxxxx>
---
net/ncsi/internal.h | 1 +
net/ncsi/ncsi-manage.c | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/net/ncsi/internal.h b/net/ncsi/internal.h
index adee6dc..cb41953 100644
--- a/net/ncsi/internal.h
+++ b/net/ncsi/internal.h
@@ -253,6 +253,7 @@ struct ncsi_package {
bool multi_channel; /* Enable multiple channels */
u32 channel_whitelist; /* Channels to configure */
struct ncsi_channel *preferred_channel; /* Primary channel */
+ struct rcu_head rcu;
};

struct ncsi_request {
diff --git a/net/ncsi/ncsi-manage.c b/net/ncsi/ncsi-manage.c
index 54d0df0..237e041 100644
--- a/net/ncsi/ncsi-manage.c
+++ b/net/ncsi/ncsi-manage.c
@@ -326,7 +326,8 @@ void ncsi_remove_package(struct ncsi_package *np)
ndp->package_num--;
spin_unlock_irqrestore(&ndp->lock, flags);

- kfree(np);
+ /* Netlink walkers may still hold the package */
+ kfree_rcu(np, rcu);
}

void ncsi_find_package_and_channel(struct ncsi_dev_priv *ndp,
--
2.43.0