* [PATCH 1/3] rtnetlink: return EMSGSIZE when first dump entry does not fit
[not found] <20261109012115.11898-1-stephen@networkplumber.org>
@ 2026-11-09 1:13 ` Stephen Hemminger
2026-11-09 1:13 ` [PATCH 2/3] ipv6: return EMSGSIZE when first route in dump " Stephen Hemminger
2026-11-09 1:13 ` [PATCH 3/3] selftests: net: netlink-dumps: test dump of route larger than skb Stephen Hemminger
2 siblings, 0 replies; 3+ messages in thread
From: Stephen Hemminger @ 2026-11-09 1:13 UTC (permalink / raw)
To: netdev
Cc: dsahern, Stephen Hemminger, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Kuniyuki Iwashima,
Ido Schimmel, Sabrina Dubroca, Andrea Parri, Stanislav Fomichev,
Nikolaos Gkarlis, Willem de Bruijn, open list
If a route entry is too large for the dump skb, the dump is silently
truncated and no error is returned.
For handlers with RTNL_FLAG_DUMP_SPLIT_NLM_DONE, rtnl_dumpit() drops
-EMSGSIZE and returns skb->len. With an empty skb this is 0, so the
dump ends with NLMSG_DONE and status 0, and the remaining entries are
lost.
The first dump skb is allocated from __netlink_dump_start() while
max_recvmsg_len is still 0, so it is only NLMSG_GOODSIZE. On a 4K page
kernel that is under 4000 bytes, and a 500 way multipath route does not
fit:
ip link add dummy0 type dummy
ip link set dummy0 up
ip route add 198.51.100.0/24 table 100 \
$(for i in $(seq 500); do echo -n "nexthop dev dummy0 "; done)
ip route show table 100
The dump prints nothing and exits 0, although "ip route get
198.51.100.1 table 100" still shows the route.
Only ignore -EMSGSIZE when the skb has data, so that an entry which
does not fit in an empty skb reports the error to user space.
Fixes: 5b4b62a169e1 ("rtnetlink: make the "split" NLM_DONE handling generic")
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
net/core/rtnetlink.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/net/core/rtnetlink.c b/net/core/rtnetlink.c
index e141340172ce..2777796a4ed2 100644
--- a/net/core/rtnetlink.c
+++ b/net/core/rtnetlink.c
@@ -7043,7 +7043,10 @@ static int rtnl_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
* Some applications which parse netlink manually depend on this.
*/
if (cb->flags & RTNL_FLAG_DUMP_SPLIT_NLM_DONE) {
- if (err < 0 && err != -EMSGSIZE)
+ /* -EMSGSIZE with nothing in the skb is an error, as returning
+ * a length of 0 would end the dump as if it was complete.
+ */
+ if (err < 0 && (err != -EMSGSIZE || !skb->len))
return err;
if (!err)
cb->data = NULL;
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 2/3] ipv6: return EMSGSIZE when first route in dump does not fit
[not found] <20261109012115.11898-1-stephen@networkplumber.org>
2026-11-09 1:13 ` [PATCH 1/3] rtnetlink: return EMSGSIZE when first dump entry does not fit Stephen Hemminger
@ 2026-11-09 1:13 ` Stephen Hemminger
2026-11-09 1:13 ` [PATCH 3/3] selftests: net: netlink-dumps: test dump of route larger than skb Stephen Hemminger
2 siblings, 0 replies; 3+ messages in thread
From: Stephen Hemminger @ 2026-11-09 1:13 UTC (permalink / raw)
To: netdev
Cc: dsahern, Stephen Hemminger, Ido Schimmel, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
open list
If a route does not fit in an empty dump skb, fib6_dump_node()
suspends the walker and inet6_dump_fib() returns 1. rtnl_dumpit()
then returns skb->len, which is 0, and the dump ends with NLMSG_DONE
and status 0, silently dropping the route and those after it.
This became reachable when multipath routes started being dumped as a
single message with an RTA_MULTIPATH attribute; a wide enough route no
longer fits.
Return -EMSGSIZE in this case, as IPv4 does. This depends on the
rtnl_dumpit() fix in the previous patch.
Fixes: beb1afac518d ("net: ipv6: Add support to dump multipath routes via RTA_MULTIPATH attribute")
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
net/ipv6/ip6_fib.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index e1d49897d6ab..1025267992f0 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -755,6 +755,12 @@ static int inet6_dump_fib(struct sk_buff *skb, struct netlink_callback *cb)
unlock:
rcu_read_unlock();
+ /* The walker stopped before adding anything, so the next route
+ * is too large for this skb. Report that instead of letting an
+ * empty skb look like the end of the dump.
+ */
+ if (err > 0 && !skb->len)
+ err = -EMSGSIZE;
if (err <= 0)
fib6_dump_end(cb);
return err;
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 3/3] selftests: net: netlink-dumps: test dump of route larger than skb
[not found] <20261109012115.11898-1-stephen@networkplumber.org>
2026-11-09 1:13 ` [PATCH 1/3] rtnetlink: return EMSGSIZE when first dump entry does not fit Stephen Hemminger
2026-11-09 1:13 ` [PATCH 2/3] ipv6: return EMSGSIZE when first route in dump " Stephen Hemminger
@ 2026-11-09 1:13 ` Stephen Hemminger
2 siblings, 0 replies; 3+ messages in thread
From: Stephen Hemminger @ 2026-11-09 1:13 UTC (permalink / raw)
To: netdev
Cc: dsahern, Stephen Hemminger, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan,
open list:KERNEL SELFTEST FRAMEWORK, open list
Add IPv4 and IPv6 multipath routes that do not fit in the first dump
skb of a fresh socket. The dump must return the route or fail with
-EMSGSIZE, not end with status 0.
IPv6 nexthops cannot use lo, so create a dummy device and generalize
lo_set_up() to if_set_up(). Add the IPv4 routing options to config.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
tools/testing/selftests/net/config | 3 +
tools/testing/selftests/net/netlink-dumps.c | 271 +++++++++++++++++++-
2 files changed, 270 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index d4416f8bdcfd..e2f136274e6d 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -29,6 +29,8 @@ CONFIG_IP6_NF_MATCH_RPFILTER=m
CONFIG_IP6_NF_NAT=m
CONFIG_IP6_NF_RAW=m
CONFIG_IP6_NF_TARGET_REJECT=m
+CONFIG_IP_ADVANCED_ROUTER=y
+CONFIG_IP_MULTIPLE_TABLES=y
CONFIG_IP_NF_FILTER=m
CONFIG_IP_NF_IPTABLES=m
CONFIG_IP_NF_IPTABLES_LEGACY=m
@@ -38,6 +40,7 @@ CONFIG_IP_NF_NAT=m
CONFIG_IP_NF_RAW=m
CONFIG_IP_NF_TARGET_REJECT=m
CONFIG_IP_NF_TARGET_TTL=m
+CONFIG_IP_ROUTE_MULTIPATH=y
CONFIG_IP_SCTP=m
CONFIG_IPV6=y
CONFIG_IPV6_FOU=m
diff --git a/tools/testing/selftests/net/netlink-dumps.c b/tools/testing/selftests/net/netlink-dumps.c
index 4a94a25167e2..43f63f3f2289 100644
--- a/tools/testing/selftests/net/netlink-dumps.c
+++ b/tools/testing/selftests/net/netlink-dumps.c
@@ -263,11 +263,11 @@ TEST(close_with_ref)
usleep(100 * 1000);
}
-static int lo_set_up(int sock, bool up)
+static int if_set_up(int sock, const char *name, bool up)
{
struct ifreq ifr = {};
- strcpy(ifr.ifr_name, "lo");
+ strcpy(ifr.ifr_name, name);
if (ioctl(sock, SIOCGIFFLAGS, &ifr))
return -1;
if (up)
@@ -321,12 +321,12 @@ TEST(dump_with_queued_ntf)
sizeof(addr)), 0);
// A large read makes the kernel attempt large dump skbs
- ASSERT_EQ(lo_set_up(inet_sock, true), 0);
+ ASSERT_EQ(if_set_up(inet_sock, "lo", 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);
+ ASSERT_EQ(if_set_up(inet_sock, "lo", false), 0);
n = send(netlink_sock, &dump_links, sizeof(dump_links), 0);
ASSERT_EQ(n, sizeof(dump_links));
@@ -362,4 +362,267 @@ TEST(dump_with_queued_ntf)
close(inet_sock);
}
+#define BIG_RT_TABLE 100
+#define BIG_RT_IFINDEX 100
+#define BIG_RT4_NH 1500
+#define BIG_RT6_NH 500
+
+static const struct {
+ struct nlmsghdr nlhdr;
+ struct ifinfomsg ifm;
+ struct nlattr name_hdr;
+ char name[8];
+ struct nlattr linkinfo_hdr;
+ struct nlattr kind_hdr;
+ char kind[8];
+} new_dummy = {
+ .nlhdr = {
+ .nlmsg_len = sizeof(new_dummy),
+ .nlmsg_type = RTM_NEWLINK,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE |
+ NLM_F_EXCL,
+ .nlmsg_seq = 1,
+ },
+ .ifm = {
+ .ifi_family = AF_UNSPEC,
+ .ifi_index = BIG_RT_IFINDEX,
+ },
+ .name_hdr = {
+ .nla_len = 4 + sizeof("dummy0"),
+ .nla_type = IFLA_IFNAME,
+ },
+ .name = "dummy0",
+ .linkinfo_hdr = {
+ .nla_len = 4 + 4 + 8,
+ .nla_type = IFLA_LINKINFO,
+ },
+ .kind_hdr = {
+ .nla_len = 4 + sizeof("dummy"),
+ .nla_type = IFLA_INFO_KIND,
+ },
+ .kind = "dummy",
+};
+
+static struct {
+ struct nlmsghdr nlhdr;
+ struct rtmsg rtm;
+ struct nlattr dst_hdr;
+ __u8 dst[4];
+ struct nlattr table_hdr;
+ __u32 table;
+ struct nlattr mp_hdr;
+ struct rtnexthop nh[BIG_RT4_NH];
+} new_route4 = {
+ .nlhdr = {
+ .nlmsg_len = sizeof(new_route4),
+ .nlmsg_type = RTM_NEWROUTE,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE |
+ NLM_F_EXCL,
+ .nlmsg_seq = 1,
+ },
+ .rtm = {
+ .rtm_family = AF_INET,
+ .rtm_dst_len = 24,
+ .rtm_protocol = RTPROT_STATIC,
+ .rtm_scope = RT_SCOPE_LINK,
+ .rtm_type = RTN_UNICAST,
+ },
+ .dst_hdr = {
+ .nla_len = 4 + 4,
+ .nla_type = RTA_DST,
+ },
+ .dst = { 198, 51, 100, 0 },
+ .table_hdr = {
+ .nla_len = 4 + 4,
+ .nla_type = RTA_TABLE,
+ },
+ .table = BIG_RT_TABLE,
+ .mp_hdr = {
+ .nla_len = 4 + sizeof(new_route4.nh),
+ .nla_type = RTA_MULTIPATH,
+ },
+};
+
+struct rtnexthop6 {
+ struct rtnexthop rtnh;
+ struct nlattr gw_hdr;
+ __u8 gw[16];
+};
+
+static struct {
+ struct nlmsghdr nlhdr;
+ struct rtmsg rtm;
+ struct nlattr dst_hdr;
+ __u8 dst[16];
+ struct nlattr table_hdr;
+ __u32 table;
+ struct nlattr mp_hdr;
+ struct rtnexthop6 nh[BIG_RT6_NH];
+} new_route6 = {
+ .nlhdr = {
+ .nlmsg_len = sizeof(new_route6),
+ .nlmsg_type = RTM_NEWROUTE,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE |
+ NLM_F_EXCL,
+ .nlmsg_seq = 1,
+ },
+ .rtm = {
+ .rtm_family = AF_INET6,
+ .rtm_dst_len = 64,
+ .rtm_protocol = RTPROT_STATIC,
+ .rtm_scope = RT_SCOPE_UNIVERSE,
+ .rtm_type = RTN_UNICAST,
+ },
+ .dst_hdr = {
+ .nla_len = 4 + 16,
+ .nla_type = RTA_DST,
+ },
+ .dst = { 0x20, 0x01, 0x0d, 0xb8 }, // 2001:db8::/64
+ .table_hdr = {
+ .nla_len = 4 + 4,
+ .nla_type = RTA_TABLE,
+ },
+ .table = BIG_RT_TABLE,
+ .mp_hdr = {
+ .nla_len = 4 + sizeof(new_route6.nh),
+ .nla_type = RTA_MULTIPATH,
+ },
+};
+
+static struct {
+ struct nlmsghdr nlhdr;
+ struct rtmsg rtm;
+} dump_routes = {
+ .nlhdr = {
+ .nlmsg_len = sizeof(dump_routes),
+ .nlmsg_type = RTM_GETROUTE,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
+ .nlmsg_seq = 2,
+ },
+ .rtm = {
+ .rtm_table = BIG_RT_TABLE,
+ },
+};
+
+static int nl_request_ack(int sock, const void *req, size_t len)
+{
+ struct nlmsghdr *nlh;
+ char buf[4096];
+ ssize_t n;
+
+ if (send(sock, req, len, 0) != (ssize_t)len)
+ return -errno;
+ n = recv(sock, buf, sizeof(buf), 0);
+ if (n < 0)
+ return -errno;
+ nlh = (struct nlmsghdr *)buf;
+ if (!NLMSG_OK(nlh, n) || nlh->nlmsg_type != NLMSG_ERROR)
+ return -EPROTO;
+ return *(int *)NLMSG_DATA(nlh);
+}
+
+// Dump table BIG_RT_TABLE on a fresh socket, so the first dump skb is
+// small, and count routes. The table holds only one route, too large for
+// that skb. Returns the status from NLMSG_DONE or NLMSG_ERROR.
+static int dump_big_table(struct __test_metadata *_metadata, int family,
+ int *routes)
+{
+ int netlink_sock, i, ret = 1;
+ char buf[32768];
+ int one = 1;
+ ssize_t n;
+
+ netlink_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
+ ASSERT_GE(netlink_sock, 0);
+ ASSERT_EQ(setsockopt(netlink_sock, SOL_NETLINK, NETLINK_GET_STRICT_CHK,
+ &one, sizeof(one)), 0);
+
+ dump_routes.rtm.rtm_family = family;
+ n = send(netlink_sock, &dump_routes, sizeof(dump_routes), 0);
+ ASSERT_EQ(n, sizeof(dump_routes));
+
+ *routes = 0;
+ for (i = 0; i < 100 && ret > 0; i++) {
+ struct nlmsghdr *nlh;
+
+ n = recv(netlink_sock, buf, sizeof(buf), 0);
+ ASSERT_GE(n, 0);
+
+ for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, n);
+ nlh = NLMSG_NEXT(nlh, n)) {
+ if (nlh->nlmsg_type == RTM_NEWROUTE)
+ (*routes)++;
+ if (nlh->nlmsg_type == NLMSG_DONE ||
+ nlh->nlmsg_type == NLMSG_ERROR) {
+ ret = *(int *)NLMSG_DATA(nlh);
+ break;
+ }
+ }
+ }
+ EXPECT_LE(ret, 0);
+
+ close(netlink_sock);
+ return ret;
+}
+
+// A route that doesn't fit in the first dump skb must either be dumped or
+// end the dump with EMSGSIZE. It must not end the dump as if complete.
+TEST(dump_large_route)
+{
+ int netlink_sock, inet_sock, i, ret, routes;
+
+ 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);
+
+ ret = nl_request_ack(netlink_sock, &new_dummy, sizeof(new_dummy));
+ if (ret == -EOPNOTSUPP)
+ SKIP(return, "Needs CONFIG_DUMMY");
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(if_set_up(inet_sock, "lo", true), 0);
+ ASSERT_EQ(if_set_up(inet_sock, "dummy0", true), 0);
+
+ for (i = 0; i < BIG_RT4_NH; i++) {
+ new_route4.nh[i].rtnh_len = sizeof(new_route4.nh[i]);
+ new_route4.nh[i].rtnh_ifindex = 1; // lo
+ }
+ ASSERT_EQ(nl_request_ack(netlink_sock, &new_route4,
+ sizeof(new_route4)), 0);
+
+ for (i = 0; i < BIG_RT6_NH; i++) {
+ struct rtnexthop6 *nh = &new_route6.nh[i];
+
+ nh->rtnh.rtnh_len = sizeof(*nh);
+ nh->rtnh.rtnh_ifindex = BIG_RT_IFINDEX;
+ nh->gw_hdr.nla_len = sizeof(nh->gw_hdr) + sizeof(nh->gw);
+ nh->gw_hdr.nla_type = RTA_GATEWAY;
+ nh->gw[0] = 0xfe; // fe80::<i + 1>
+ nh->gw[1] = 0x80;
+ nh->gw[14] = (i + 1) >> 8;
+ nh->gw[15] = (i + 1) & 0xff;
+ }
+ ASSERT_EQ(nl_request_ack(netlink_sock, &new_route6,
+ sizeof(new_route6)), 0);
+
+ // The ACK reads above used large buffers, which raise the dump
+ // skb size for this socket. Dump on fresh sockets instead.
+ close(netlink_sock);
+
+ ret = dump_big_table(_metadata, AF_INET, &routes);
+ if (ret != -EMSGSIZE)
+ EXPECT_EQ(routes, 1);
+
+ ret = dump_big_table(_metadata, AF_INET6, &routes);
+ if (ret != -EMSGSIZE)
+ EXPECT_EQ(routes, 1);
+
+ close(inet_sock);
+}
+
TEST_HARNESS_MAIN
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-09 1:21 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
[not found] <20261109012115.11898-1-stephen@networkplumber.org>
2026-11-09 1:13 ` [PATCH 1/3] rtnetlink: return EMSGSIZE when first dump entry does not fit Stephen Hemminger
2026-11-09 1:13 ` [PATCH 2/3] ipv6: return EMSGSIZE when first route in dump " Stephen Hemminger
2026-11-09 1:13 ` [PATCH 3/3] selftests: net: netlink-dumps: test dump of route larger than skb Stephen Hemminger
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®