From b45d37740cfb4bdfccef3fbd9fd970fd3224438f Mon Sep 17 00:00:00 2001 From: Jhonata Poma-Hansen Date: Tue, 27 Oct 2026 15:04:01 +0100 Subject: netlink: check sk_rcvbuf before charging the dump skb Since commit ae8f160e7eb2 ("netlink: Fix wraparounds of sk->sk_rmem_alloc."), netlink_dump() charges the dump skb first and queues it only if sk_rmem_alloc is still below sk_rcvbuf. Commit a215b5723922 ("netlink: make sure we allow at least one dump skb") added an exception for an empty socket. A dump that starts while something else is still charged to a socket with a small receive buffer fails with ENOBUFS and stays running. Every recvmsg() retries it, fails again and sets sk_err, so a reader that peeks before reading, as libnl does, gets ENOBUFS on the read after each peek, never consumes the message at the head of the queue and spins forever. I hit this with "iw dev wlan0 scan" hanging at 100% CPU on an arm64 board with a 6.6 based kernel (iw 5.19 sets SO_RCVBUF to 8KiB). A kprobe trace showed the scan dump starting while the NEW_SCAN_RESULTS notification iw had just read was still charged to its socket, because iw had preempted the sender before netlink_broadcast() returned. Check the limit before charging, as before ae8f160e7eb2, which also covers the empty socket. netlink_recvmsg() continues a dump only at or below half of sk_rcvbuf, so the skb is then always queued, and a dump goes over the limit by at most one skb. Refuse a skb that would take sk_rmem_alloc past INT_MAX, so the wraparound stays fixed. Reproduced on net in a KVM guest with a userspace reproducer and the selftest in the next patch; both pass with this fix. Fixes: ae8f160e7eb2 ("netlink: Fix wraparounds of sk->sk_rmem_alloc.") Signed-off-by: Jhonata Poma-Hansen Reviewed-by: Eric Dumazet Link: https://patch.msgid.link/20261027-netlink-dump-livelock-v2-1-c4bb05352a33@gmail.com Signed-off-by: Jakub Kicinski --- net/netlink/af_netlink.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/net/netlink/af_netlink.c b/net/netlink/af_netlink.c index fec2879c497d..1281c42a3456 100644 --- a/net/netlink/af_netlink.c +++ b/net/netlink/af_netlink.c @@ -2322,9 +2322,13 @@ static int netlink_dump(struct sock *sk, bool lock_taken) if (!skb) goto errout_skb; + /* Check the limit before charging the skb: netlink_recvmsg() continues + * the dump at or below half of sk_rcvbuf, and the skb must be queued + * then. Keep sk_rmem_alloc at or below INT_MAX. + */ rcvbuf = READ_ONCE(sk->sk_rcvbuf); rmem = atomic_add_return(skb->truesize, &sk->sk_rmem_alloc); - if (rmem != skb->truesize && rmem >= rcvbuf) { + if (rmem - skb->truesize >= rcvbuf || rmem > INT_MAX) { atomic_sub(skb->truesize, &sk->sk_rmem_alloc); goto errout_skb; } -- cgit v1.2.3 From e615d0a8b20161fd470ca2748001105bcd3c44b1 Mon Sep 17 00:00:00 2001 From: Jhonata Poma-Hansen Date: Tue, 27 Oct 2026 15:04:02 +0100 Subject: selftests: net: netlink-dumps: start a dump with a notification queued Add a test which starts a link dump on a socket with SO_RCVBUF 2048 while a link notification is still queued, after a large read made the kernel attempt large dump skbs. It reads like libnl does, peeking before each read, and expects the dump to complete with a zero status and at least one link. The notifications come from bringing lo up and down, so the test runs in its own netns and is skipped without the privileges to create one. The test fails without the previous patch. Signed-off-by: Jhonata Poma-Hansen Link: https://patch.msgid.link/20261027-netlink-dump-livelock-v2-2-c4bb05352a33@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/net/netlink-dumps.c | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/tools/testing/selftests/net/netlink-dumps.c b/tools/testing/selftests/net/netlink-dumps.c index 51129c564d0a..4a94a25167e2 100644 --- a/tools/testing/selftests/net/netlink-dumps.c +++ b/tools/testing/selftests/net/netlink-dumps.c @@ -2,9 +2,12 @@ #define _GNU_SOURCE +#include #include +#include #include #include +#include #include #include #include @@ -260,4 +263,103 @@ TEST(close_with_ref) usleep(100 * 1000); } +static int lo_set_up(int sock, bool up) +{ + struct ifreq ifr = {}; + + strcpy(ifr.ifr_name, "lo"); + if (ioctl(sock, SIOCGIFFLAGS, &ifr)) + return -1; + if (up) + ifr.ifr_flags |= IFF_UP; + else + ifr.ifr_flags &= ~IFF_UP; + return ioctl(sock, SIOCSIFFLAGS, &ifr); +} + +static const struct { + struct nlmsghdr nlhdr; + struct ifinfomsg ifm; +} dump_links = { + .nlhdr = { + .nlmsg_len = sizeof(dump_links), + .nlmsg_type = RTM_GETLINK, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP, + .nlmsg_seq = 1, + }, + .ifm = { + .ifi_family = AF_UNSPEC, + }, +}; + +// A dump started while a notification is still queued must make progress +// for a reader which peeks before reading, like libnl does +TEST(dump_with_queued_ntf) +{ + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + .nl_groups = RTMGRP_LINK, + }; + int netlink_sock, inet_sock, i, ret, links = 0; + int rcvbuf = 2048; + bool done = false; + char buf[32768]; + ssize_t n; + + ret = unshare(CLONE_NEWNET); + if (ret && errno == EPERM) + SKIP(return, "Needs CAP_SYS_ADMIN for a netns"); + ASSERT_EQ(ret, 0); + + inet_sock = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_GE(inet_sock, 0); + netlink_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + ASSERT_GE(netlink_sock, 0); + ASSERT_EQ(setsockopt(netlink_sock, SOL_SOCKET, SO_RCVBUF, + &rcvbuf, sizeof(rcvbuf)), 0); + ASSERT_EQ(bind(netlink_sock, (struct sockaddr *)&addr, + sizeof(addr)), 0); + + // A large read makes the kernel attempt large dump skbs + ASSERT_EQ(lo_set_up(inet_sock, true), 0); + n = recv(netlink_sock, buf, sizeof(buf), 0); + ASSERT_GE(n, (ssize_t)sizeof(struct nlmsghdr)); + + // Leave one notification queued, then start the dump + ASSERT_EQ(lo_set_up(inet_sock, false), 0); + n = send(netlink_sock, &dump_links, sizeof(dump_links), 0); + ASSERT_EQ(n, sizeof(dump_links)); + + for (i = 0; i < 100 && !done; i++) { + struct nlmsghdr *nlh; + + n = recv(netlink_sock, buf, sizeof(buf), + MSG_PEEK | MSG_TRUNC | MSG_DONTWAIT); + if (n < 0) + continue; + n = recv(netlink_sock, buf, sizeof(buf), MSG_DONTWAIT); + if (n < 0) + continue; + + for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, n); + nlh = NLMSG_NEXT(nlh, n)) { + if (nlh->nlmsg_seq != dump_links.nlhdr.nlmsg_seq) + continue; + if (nlh->nlmsg_type == NLMSG_ERROR) + EXPECT_EQ(*(int *)NLMSG_DATA(nlh), 0); + if (nlh->nlmsg_type == RTM_NEWLINK) + links++; + if (nlh->nlmsg_type == NLMSG_DONE) { + EXPECT_EQ(*(int *)NLMSG_DATA(nlh), 0); + done = true; + } + } + } + EXPECT_TRUE(done); + EXPECT_GE(links, 1); + + close(netlink_sock); + close(inet_sock); +} + TEST_HARNESS_MAIN -- cgit v1.2.3