* [PATCH net-next v3 0/2] seg6: skip route refcounts in seg6local lookups
@ 2026-10-15 14:39 Yuya Kusakabe
2026-10-15 14:39 ` [PATCH net-next v3 1/2] seg6: skip the route refcount in seg6local IPv6 lookups Yuya Kusakabe
2026-10-15 14:39 ` [PATCH net-next v3 2/2] seg6: skip the route refcount in seg6local IPv4 lookups Yuya Kusakabe
0 siblings, 2 replies; 4+ messages in thread
From: Yuya Kusakabe @ 2026-10-15 14:39 UTC (permalink / raw)
To: Andrea Mayer, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman
Cc: netdev, linux-kernel, Gabriel Goller, Yuya Kusakabe
seg6local looks up the nexthop of its IPv6 and IPv4 behaviors from the
RCU read-side section of the receive path, yet takes and drops a route
reference for every packet. Skip the reference, as ip6_route_input()
and ip_rcv_finish() do.
---
Changes in v3:
- Keep the refcounted lookup in seg6_lookup_nexthop() and give
seg6_lookup_any_nexthop() a noref argument instead, so the BPF LWT
helpers see no change while a route is released (Andrea Mayer,
Gabriel Goller, Sashiko).
- Mention NFQUEUE reinjection and the release race in the commit
messages (Andrea Mayer).
- Link to v2: https://patch.msgid.link/20261010-seg6-lookup-noref-v2-0-51c09dd98197@gmail.com
Changes in v2:
- Split the lookup into a noref primitive for the seg6local input
actions and a refcounted seg6_lookup_nexthop() wrapper, which the BPF
LWT helpers also call (Andrea Mayer, Sashiko).
- Rebase onto net-next and re-measure.
- Link to v1: https://patch.msgid.link/20261005-seg6-lookup-noref-v1-0-092e84774e80@gmail.com
---
Yuya Kusakabe (2):
seg6: skip the route refcount in seg6local IPv6 lookups
seg6: skip the route refcount in seg6local IPv4 lookups
net/ipv6/seg6_local.c | 52 ++++++++++++++++++++++++++++++++++++---------------
1 file changed, 37 insertions(+), 15 deletions(-)
---
base-commit: 758fe10f83a655bed0f7b98fcfacdfc3556fb9a1
change-id: 20261004-seg6-lookup-noref-e016495b60ed
Best regards,
--
Yuya Kusakabe <yuya.kusakabe@gmail.com>
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH net-next v3 1/2] seg6: skip the route refcount in seg6local IPv6 lookups
2026-10-15 14:39 [PATCH net-next v3 0/2] seg6: skip route refcounts in seg6local lookups Yuya Kusakabe
@ 2026-10-15 14:39 ` Yuya Kusakabe
2026-10-19 23:04 ` Andrea Mayer
2026-10-15 14:39 ` [PATCH net-next v3 2/2] seg6: skip the route refcount in seg6local IPv4 lookups Yuya Kusakabe
1 sibling, 1 reply; 4+ messages in thread
From: Yuya Kusakabe @ 2026-10-15 14:39 UTC (permalink / raw)
To: Andrea Mayer, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman
Cc: netdev, linux-kernel, Gabriel Goller, Yuya Kusakabe
ip6_route_input() attaches the input route to the skb without taking a
reference, relying on the RCU read-side section the lookup runs in.
The seg6local input actions also look the route up and hand the skb to
dst_input() within one RCU read-side section: the one of the receive
path, or that of the nfnetlink verdict handler for a packet reinjected
from NFQUEUE. Yet seg6_lookup_any_nexthop() takes and drops a route
reference for every packet.
Give seg6_lookup_any_nexthop() a noref argument, and set it for the
input and table lookups of those actions, as ip6_route_input() does.
A packet that races with the release of its route can then be dropped
without an ICMPv6 error, where ip6_hold_safe() used to send it on to
the next rule or to ip6_null_entry; ip6_route_input() has the same
window. seg6_lookup_nexthop(), which the BPF LWT helpers also call,
keeps the refcounted lookup: BPF_PROG_TEST_RUN runs them repeatedly on
the same skb and may leave the RCU read-side section between runs.
Throughput at 0.5% packet loss of 94-byte End packets (two-segment
SRH, no payload), spread by RSS on an ixgbe 82599ES across two 2.30 GHz
Xeon E5-2650 v3 with 10 cores each, offered by TRex and binary-searched
over 10 runs of 10 s, in Mpps:
queues cores before after
1 1 0.740 0.756 (+2.2%)
10 10 on the NIC's socket 6.404 6.950 (+8.5%)
16 10 + 6 on two sockets 6.550 10.124 (+54.6%)
With the default rules, fib6_rule_lookup() tries the local table
first, and the miss takes and drops a reference on ip6_null_entry,
one route shared by every CPU. At 16 queues that cache line bounces
between the sockets, and ip6_hold_safe() and dst_release() together
take about a third of the cycles.
Assisted-by: LLM
Signed-off-by: Yuya Kusakabe <yuya.kusakabe@gmail.com>
---
net/ipv6/seg6_local.c | 48 +++++++++++++++++++++++++++++++++++-------------
1 file changed, 35 insertions(+), 13 deletions(-)
diff --git a/net/ipv6/seg6_local.c b/net/ipv6/seg6_local.c
index 0c3d1be7f129..965263ff65d3 100644
--- a/net/ipv6/seg6_local.c
+++ b/net/ipv6/seg6_local.c
@@ -319,7 +319,7 @@ static struct ipv6_sr_hdr *advance_nextseg(struct sk_buff *skb,
static int
seg6_lookup_any_nexthop(struct sk_buff *skb, struct in6_addr *nhaddr,
- u32 tbl_id, bool local_delivery, int oif)
+ u32 tbl_id, bool local_delivery, int oif, bool noref)
{
struct net *net = dev_net(skb->dev);
struct ipv6hdr *hdr = ipv6_hdr(skb);
@@ -342,6 +342,8 @@ seg6_lookup_any_nexthop(struct sk_buff *skb, struct in6_addr *nhaddr,
fl6.flowi6_flags = FLOWI_FLAG_KNOWN_NH;
if (!tbl_id && !oif) {
+ if (noref)
+ flags |= RT6_LOOKUP_F_DST_NOREF;
dst = ip6_route_input_lookup(net, skb->dev, &fl6, skb, flags);
} else if (tbl_id) {
struct fib6_table *table;
@@ -350,6 +352,8 @@ seg6_lookup_any_nexthop(struct sk_buff *skb, struct in6_addr *nhaddr,
if (!table)
goto out;
+ if (noref)
+ flags |= RT6_LOOKUP_F_DST_NOREF;
rt = ip6_pol_route(net, table, oif, &fl6, skb, flags);
dst = &rt->dst;
} else {
@@ -363,7 +367,7 @@ seg6_lookup_any_nexthop(struct sk_buff *skb, struct in6_addr *nhaddr,
dev_flags |= IFF_LOOPBACK;
if (dst && (dst_dev(dst)->flags & dev_flags) && !dst->error) {
- dst_release(dst);
+ ip6_rt_put_flags(dst_rt6_info(dst), flags);
dst = NULL;
}
@@ -372,17 +376,35 @@ seg6_lookup_any_nexthop(struct sk_buff *skb, struct in6_addr *nhaddr,
rt = net->ipv6.ip6_blk_hole_entry;
dst = &rt->dst;
dst_hold(dst);
+ flags &= ~RT6_LOOKUP_F_DST_NOREF;
}
skb_dst_drop(skb);
- skb_dst_set(skb, dst);
+ if ((flags & RT6_LOOKUP_F_DST_NOREF) && !dst->rt_uncached_list)
+ skb_dst_set_noref(skb, dst);
+ else
+ skb_dst_set(skb, dst);
return dst->error;
}
+static int
+seg6_lookup_any_nexthop_noref(struct sk_buff *skb, struct in6_addr *nhaddr,
+ u32 tbl_id, bool local_delivery, int oif)
+{
+ return seg6_lookup_any_nexthop(skb, nhaddr, tbl_id, local_delivery, oif,
+ true);
+}
+
+static int seg6_lookup_nexthop_noref(struct sk_buff *skb,
+ struct in6_addr *nhaddr, u32 tbl_id)
+{
+ return seg6_lookup_any_nexthop_noref(skb, nhaddr, tbl_id, false, 0);
+}
+
int seg6_lookup_nexthop(struct sk_buff *skb,
struct in6_addr *nhaddr, u32 tbl_id)
{
- return seg6_lookup_any_nexthop(skb, nhaddr, tbl_id, false, 0);
+ return seg6_lookup_any_nexthop(skb, nhaddr, tbl_id, false, 0, false);
}
static __u8 seg6_flv_lcblock_octects(const struct seg6_flavors_info *finfo)
@@ -446,7 +468,7 @@ seg6_next_csid_advance_arg(struct sk_buff *skb,
static int input_action_end_finish(struct sk_buff *skb,
struct seg6_local_lwt *slwt)
{
- seg6_lookup_nexthop(skb, NULL, 0);
+ seg6_lookup_nexthop_noref(skb, NULL, 0);
return dst_input(skb);
}
@@ -497,7 +519,7 @@ static int end_next_csid_core(struct sk_buff *skb, struct seg6_local_lwt *slwt)
static int input_action_end_x_finish(struct sk_buff *skb,
struct seg6_local_lwt *slwt)
{
- seg6_lookup_any_nexthop(skb, &slwt->nh6, 0, false, slwt->oif);
+ seg6_lookup_any_nexthop_noref(skb, &slwt->nh6, 0, false, slwt->oif);
return dst_input(skb);
}
@@ -939,7 +961,7 @@ static int input_action_end_t(struct sk_buff *skb, struct seg6_local_lwt *slwt)
goto drop;
}
- seg6_lookup_nexthop(skb, NULL, slwt->table);
+ seg6_lookup_nexthop_noref(skb, NULL, slwt->table);
return dst_input(skb);
@@ -1023,7 +1045,7 @@ static int input_action_end_dx6_finish(struct net *net, struct sock *sk,
if (!ipv6_addr_any(&slwt->nh6))
nhaddr = &slwt->nh6;
- seg6_lookup_nexthop(skb, nhaddr, 0);
+ seg6_lookup_nexthop_noref(skb, nhaddr, 0);
return dst_input(skb);
}
@@ -1383,7 +1405,7 @@ static int input_action_end_dt6(struct sk_buff *skb,
/* note: this time we do not need to specify the table because the VRF
* takes care of selecting the correct table.
*/
- seg6_lookup_any_nexthop(skb, NULL, 0, true, 0);
+ seg6_lookup_any_nexthop_noref(skb, NULL, 0, true, 0);
return dst_input(skb);
@@ -1391,7 +1413,7 @@ static int input_action_end_dt6(struct sk_buff *skb,
#endif
skb_set_transport_header(skb, sizeof(struct ipv6hdr));
- seg6_lookup_any_nexthop(skb, NULL, slwt->table, true, 0);
+ seg6_lookup_any_nexthop_noref(skb, NULL, slwt->table, true, 0);
return dst_input(skb);
@@ -1446,7 +1468,7 @@ static int input_action_end_b6(struct sk_buff *skb, struct seg6_local_lwt *slwt)
skb_set_transport_header(skb, sizeof(struct ipv6hdr));
- seg6_lookup_nexthop(skb, NULL, 0);
+ seg6_lookup_nexthop_noref(skb, NULL, 0);
return dst_input(skb);
@@ -1482,7 +1504,7 @@ static int input_action_end_b6_encap(struct sk_buff *skb,
skb_set_transport_header(skb, sizeof(struct ipv6hdr));
- seg6_lookup_nexthop(skb, NULL, 0);
+ seg6_lookup_nexthop_noref(skb, NULL, 0);
return dst_input(skb);
@@ -1571,7 +1593,7 @@ static int input_action_end_bpf(struct sk_buff *skb,
local_unlock_nested_bh(&seg6_bpf_srh_states.bh_lock);
if (ret != BPF_REDIRECT)
- seg6_lookup_nexthop(skb, NULL, 0);
+ seg6_lookup_nexthop_noref(skb, NULL, 0);
return dst_input(skb);
--
2.50.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH net-next v3 2/2] seg6: skip the route refcount in seg6local IPv4 lookups
2026-10-15 14:39 [PATCH net-next v3 0/2] seg6: skip route refcounts in seg6local lookups Yuya Kusakabe
2026-10-15 14:39 ` [PATCH net-next v3 1/2] seg6: skip the route refcount in seg6local IPv6 lookups Yuya Kusakabe
@ 2026-10-15 14:39 ` Yuya Kusakabe
1 sibling, 0 replies; 4+ messages in thread
From: Yuya Kusakabe @ 2026-10-15 14:39 UTC (permalink / raw)
To: Andrea Mayer, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman
Cc: netdev, linux-kernel, Gabriel Goller, Yuya Kusakabe
ip_route_input() looks the input route up with ip_route_input_noref()
and then forces a reference on it, which is dropped again when the
packet is transmitted. End.DX4 and End.DT4 call it and hand the skb
to dst_input() within one RCU read-side section: the one of the
receive path, or that of the nfnetlink verdict handler for a packet
reinjected from NFQUEUE.
Use ip_route_input_noref(), as ip_rcv_finish() does.
Throughput at 0.5% packet loss of 82-byte IPv4-in-IPv6 UDP packets
(no SRH), spread by RSS on an ixgbe 82599ES across two 10-core Xeon
E5-2650 v3, offered by TRex over 10 runs of 10 s, in Mpps:
End.DX4
queues cores before after
1 1 0.791 0.811 (+2.5%)
10 10 on the NIC's socket 6.118 7.394 (+20.9%)
16 10 + 6 on two sockets 3.305 10.205 (+208.8%)
End.DT4
queues cores before after
1 1 0.481 0.489 (+1.6%)
10 10 on the NIC's socket 4.328 4.588 (+6.0%)
16 10 + 6 on two sockets 3.351 7.153 (+113.5%)
Every CPU takes and drops a reference on the same input route, the
one cached on the nexthop. At 16 queues that cache line bounces
between the sockets, and skb_dst_force() and dst_release() together
take more than half of the cycles for End.DX4 and over 40% for End.DT4.
Assisted-by: LLM
Signed-off-by: Yuya Kusakabe <yuya.kusakabe@gmail.com>
---
net/ipv6/seg6_local.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/net/ipv6/seg6_local.c b/net/ipv6/seg6_local.c
index 965263ff65d3..66b22c61b835 100644
--- a/net/ipv6/seg6_local.c
+++ b/net/ipv6/seg6_local.c
@@ -1095,7 +1095,7 @@ static int input_action_end_dx4_finish(struct net *net, struct sock *sk,
skb_dst_drop(skb);
- reason = ip_route_input(skb, nhaddr, iph->saddr, 0, skb->dev);
+ reason = ip_route_input_noref(skb, nhaddr, iph->saddr, 0, skb->dev);
if (reason) {
kfree_skb_reason(skb, reason);
return -EINVAL;
@@ -1321,7 +1321,7 @@ static int input_action_end_dt4(struct sk_buff *skb,
iph = ip_hdr(skb);
- reason = ip_route_input(skb, iph->daddr, iph->saddr, 0, skb->dev);
+ reason = ip_route_input_noref(skb, iph->daddr, iph->saddr, 0, skb->dev);
if (unlikely(reason))
goto drop;
--
2.50.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net-next v3 1/2] seg6: skip the route refcount in seg6local IPv6 lookups
2026-10-15 14:39 ` [PATCH net-next v3 1/2] seg6: skip the route refcount in seg6local IPv6 lookups Yuya Kusakabe
@ 2026-10-19 23:04 ` Andrea Mayer
0 siblings, 0 replies; 4+ messages in thread
From: Andrea Mayer @ 2026-10-19 23:04 UTC (permalink / raw)
To: Yuya Kusakabe
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, netdev, linux-kernel, Gabriel Goller, Andrea Mayer
On Thu, 15 Oct 2026 23:39:27 +0900
Yuya Kusakabe <yuya.kusakabe@gmail.com> wrote:
Hi Yuya,
Thanks for v3 and considering my comments.
> [snip]
> if (!table)
> goto out;
>
> + if (noref)
> + flags |= RT6_LOOKUP_F_DST_NOREF;
Even with the change below, the "out:" branch still takes a reference when
fib6_get_table() returns NULL, because RT6_LOOKUP_F_DST_NOREF is set only
after that call. Setting RT6_LOOKUP_F_DST_NOREF before fib6_get_table(),
inside the table branch, would avoid that reference too. The oif branch
needs the flag clear, because ip6_route_output() returns the dst with a
reference.
> [snip]
> rt = net->ipv6.ip6_blk_hole_entry;
> dst = &rt->dst;
> dst_hold(dst);
> + flags &= ~RT6_LOOKUP_F_DST_NOREF;
> }
With this patch applied, the end of seg6_lookup_any_nexthop() reads:
out:
if (!dst) {
rt = net->ipv6.ip6_blk_hole_entry;
dst = &rt->dst;
dst_hold(dst);
flags &= ~RT6_LOOKUP_F_DST_NOREF;
}
skb_dst_drop(skb);
if ((flags & RT6_LOOKUP_F_DST_NOREF) && !dst->rt_uncached_list)
skb_dst_set_noref(skb, dst);
else
skb_dst_set(skb, dst);
return dst->error;
After a noref lookup, the "out:" branch still takes a reference on
ip6_blk_hole_entry and then clears RT6_LOOKUP_F_DST_NOREF.
This branch can be taken at steady state. The noref lookups, except the
ones in input_action_end_dt6(), run with local_delivery false. For example,
with End and a next segment that is a local address of the node, the lookup
returns a local route, seg6_lookup_any_nexthop() discards it, and every
packet takes and drops a reference on ip6_blk_hole_entry in the "out:"
branch. The commit message describes the same pattern for ip6_null_entry.
For a blackhole rule, __fib6_rule_action() picks ip6_blk_hole_entry and
takes no reference on it when the lookup has RT6_LOOKUP_F_DST_NOREF. The
"out:" branch could do the same, for example:
if (!dst) {
dst = &net->ipv6.ip6_blk_hole_entry->dst;
if (!(flags & RT6_LOOKUP_F_DST_NOREF))
dst_hold(dst);
}
RT6_LOOKUP_F_DST_NOREF then stays set, and the skb_dst_set_noref() at the
end of the function attaches ip6_blk_hole_entry without a reference. The
line that clears RT6_LOOKUP_F_DST_NOREF is removed as well.
What do you think?
Nit: seg6_lookup_any_nexthop_noref() and seg6_lookup_nexthop_noref() can
attach the dst without a reference. They rely on being called under RCU. A
one line comment about this, like the one ip6_route_input() has, could help
the next reader.
Ciao,
Andrea
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-19 23:05 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-15 14:39 [PATCH net-next v3 0/2] seg6: skip route refcounts in seg6local lookups Yuya Kusakabe
2026-10-15 14:39 ` [PATCH net-next v3 1/2] seg6: skip the route refcount in seg6local IPv6 lookups Yuya Kusakabe
2026-10-19 23:04 ` Andrea Mayer
2026-10-15 14:39 ` [PATCH net-next v3 2/2] seg6: skip the route refcount in seg6local IPv4 lookups Yuya Kusakabe
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®