* [PATCH iproute2] ip: flush nexthop-object routes without compatibility attributes
@ 2026-10-18 3:00 Yuen Su
2026-10-25 17:47 ` Stephen Hemminger
0 siblings, 1 reply; 2+ messages in thread
From: Yuen Su @ 2026-10-18 3:00 UTC (permalink / raw)
To: netdev; +Cc: Stephen Hemminger
Route dumps carry RTA_NH_ID together with compatibility-expanded nexthop
attributes. Copying both into RTM_DELROUTE is rejected. Compact the
flush copy while preserving the exact ID and route selectors, use its
new length for batching, and leave the original dump intact for verbose
display.
IPv4 blackhole objects synthesize a route type which may differ from the
FIB alias. Without a type selector, omit that synthetic constraint.
Retain explicit type filters so a typed flush cannot delete another
alias sharing the prefix, metric and nexthop ID. An ambiguous typed
request remains a conservative failure.
Add real-kernel regressions for IPv4/IPv6 single nexthops, groups,
ordinary multipath, mixed flush batches, blackhole aliases, cross-family
gateways, exact metric/table retention and type-filter preservation.
Signed-off-by: Yuen Su <zypulsecore@gmail.com>
---
Validation (outside the commit message):
- Native Linux 7.0.0-38: 34 semantic assertions in an isolated netns;
the same complete ip binary matrix passes ASan/UBSan with default checks.
- Public native/kernel/sanitizer CI:
https://github.com/ZyPulse-zy/iproute2/actions/runs/39084138700
- Commit-message-only revision; identical whole source tree verified by:
https://github.com/ZyPulse-zy/iproute2/actions/runs/39084640319
- AArch64/musl full ip cross-build and the same 34 assertions pass using
QEMU for 68 route/device commands; 14 nexthop-object fixture commands
use native ip because QEMU nexthop management returned ENOTSUP.
- Explicitly typed ambiguous IPv4 blackhole-object aliases remain a
conservative failure. No production router or NSS firmware was tested.
Prepared with AI assistance and checked with real-kernel regression tests.
ip/iproute.c | 39 ++++++-
testsuite/tests/ip/route/flush_nexthop.t | 136 +++++++++++++++++++++++
2 files changed, 174 insertions(+), 1 deletion(-)
create mode 100755 testsuite/tests/ip/route/flush_nexthop.t
diff --git a/ip/iproute.c b/ip/iproute.c
index 1ce71f7..22713db 100644
--- a/ip/iproute.c
+++ b/ip/iproute.c
@@ -766,6 +766,38 @@ static void print_rta_multipath(FILE *fp, const
struct rtmsg *r,
close_json_array(PRINT_JSON, NULL);
}
+static void flush_nexthop_compat(struct nlmsghdr *n,
+ const struct nlmsghdr *source)
+{
+ const struct rtmsg *r = NLMSG_DATA(source);
+ struct rtmsg *flushed = NLMSG_DATA(n);
+ const struct rtattr *rta = RTM_RTA(r);
+ int len = RTM_PAYLOAD(source);
+ char *dst = (char *)n + NLMSG_LENGTH(sizeof(*r));
+
+ for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) {
+ switch (rta->rta_type & NLA_TYPE_MASK) {
+ case RTA_OIF:
+ case RTA_GATEWAY:
+ case RTA_MULTIPATH:
+ case RTA_VIA:
+ case RTA_ENCAP_TYPE:
+ case RTA_ENCAP:
+ continue;
+ }
+ memcpy(dst, rta, RTA_ALIGN(rta->rta_len));
+ dst += RTA_ALIGN(rta->rta_len);
+ }
+ n->nlmsg_len = dst - (char *)n;
+ /* IPv4 displays a blackhole object as RTN_BLACKHOLE regardless of
+ * the FIB alias's original type. Without a type selector, omit that
+ * synthesized constraint. Never weaken an explicit type filter.
+ */
+ if (!filter.typemask &&
+ r->rtm_family == AF_INET && r->rtm_type == RTN_BLACKHOLE)
+ flushed->rtm_type = RTN_UNSPEC;
+}
+
int print_route(struct nlmsghdr *n, void *arg)
{
FILE *fp = (FILE *)arg;
@@ -810,10 +842,15 @@ int print_route(struct nlmsghdr *n, void *arg)
}
fn = (struct nlmsghdr *)(filter.flushb + NLMSG_ALIGN(filter.flushp));
memcpy(fn, n, n->nlmsg_len);
+ /* Keep the exact nexthop ID; its compatibility expansion conflicts
+ * with that ID in the delete request.
+ */
+ if (tb[RTA_NH_ID])
+ flush_nexthop_compat(fn, n);
fn->nlmsg_type = RTM_DELROUTE;
fn->nlmsg_flags = NLM_F_REQUEST;
fn->nlmsg_seq = ++rth.seq;
- filter.flushp = (((char *)fn) + n->nlmsg_len) - filter.flushb;
+ filter.flushp = (((char *)fn) + fn->nlmsg_len) - filter.flushb;
filter.flushed++;
if (show_stats < 2)
return 0;
diff --git a/testsuite/tests/ip/route/flush_nexthop.t
b/testsuite/tests/ip/route/flush_nexthop.t
new file mode 100755
index 0000000..212643a
--- /dev/null
+++ b/testsuite/tests/ip/route/flush_nexthop.t
@@ -0,0 +1,136 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+
+. lib/generic.sh
+
+# The testsuite normally supplies IP and runs each test under unshare -n.
+[ -n "$IP" ] || ts_skip
+if [ -n "$IPROUTE_TEST_PARENT_NETNS" ] &&
+ [ "$(readlink /proc/self/ns/net)" = "$IPROUTE_TEST_PARENT_NETNS" ]; then
+ ts_err "Refusing route writes in the parent network namespace"
+ exit 1
+fi
+
+ts_log "[Testing route flush with nexthop objects and compatibility
attributes]"
+DEV=nhflush0
+ts_ip "$0" "Bring namespace loopback up" link set lo up
+ts_ip "$0" "Create private dummy device" link add "$DEV" type dummy
+ts_ip "$0" "Bring device up" link set "$DEV" up
+
+for family in 4 6; do
+ if [ "$family" = 4 ]; then
+ addr=192.0.2.2/24
+ gw=192.0.2.1
+ gw2=192.0.2.3
+ prefix=203.0.113.0/24
+ group_prefix=198.51.100.0/24
+ plain_prefix=198.18.0.0/24
+ nh=101
+ nh2=102
+ group=103
+ else
+ addr=2001:db8:1::2/64
+ gw=2001:db8:1::1
+ gw2=2001:db8:1::3
+ prefix=2001:db8:2::/64
+ group_prefix=2001:db8:3::/64
+ plain_prefix=2001:db8:4::/64
+ nh=201
+ nh2=202
+ group=203
+ fi
+ if [ "$family" = 6 ]; then
+ ts_ip "$0" "Assign IPv6 address" -6 addr add "$addr" dev "$DEV" nodad
+ else
+ ts_ip "$0" "Assign IPv4 address" -4 addr add "$addr" dev "$DEV"
+ fi
+ ts_ip "$0" "Create first nexthop" -"$family" nexthop add id "$nh"
via "$gw" dev "$DEV" onlink
+ ts_ip "$0" "Create second nexthop" -"$family" nexthop add id "$nh2"
via "$gw2" dev "$DEV"
+ ts_ip "$0" "Create nexthop group" nexthop add id "$group" group "$nh/$nh2"
+ ts_ip "$0" "Add selected single-nexthop route" -"$family" route add
table 111 "$prefix" nhid "$nh" metric 10 mtu 1400 advmss 1360
+ ts_ip "$0" "Add another metric on same prefix" -"$family" route add
table 111 "$prefix" nhid "$nh2" metric 20
+ ts_ip "$0" "Add same route in another table" -"$family" route add
table 112 "$prefix" nhid "$nh" metric 10
+ ts_ip "$0" "Flush selected metric only" -"$family" route flush table
111 metric 10
+ ts_ip "$0" "Check other metric survived" -"$family" route show table
111 "$prefix"
+ test_on "nhid $nh2"
+ test_on "metric 20"
+ test_lines_count 1
+ ts_ip "$0" "Check other table survived" -"$family" route show table
112 "$prefix"
+ test_on "nhid $nh"
+ test_lines_count 1
+ ts_ip "$0" "Add multipath nexthop-object route" -"$family" route add
table 111 "$group_prefix" nhid "$group" metric 30
+ ts_ip "$0" "Flush group with verbose display of original dump"
-"$family" -s -s route flush table 111 to "$group_prefix"
+ test_on "nhid $group"
+ ts_ip "$0" "Check group route was removed" -"$family" route show
table 111 "$group_prefix"
+ test_lines_count 0
+ ts_ip "$0" "Check nexthop group itself survived" nexthop show id "$group"
+ test_on "id $group group"
+ ts_ip "$0" "Add ordinary multipath route without ID" -"$family"
route add table 111 "$plain_prefix" nexthop via "$gw" dev "$DEV"
weight 1 nexthop via "$gw2" dev "$DEV" weight 2
+ ts_ip "$0" "Flush ordinary multipath route" -"$family" route flush
table 111 to "$plain_prefix"
+ ts_ip "$0" "Check ordinary route removed" -"$family" route show
table 111 "$plain_prefix"
+ test_lines_count 0
+ # One flush batch mixes compacted single/group messages with an unchanged
+ # ordinary route. The next message must start at the new compacted length.
+ ts_ip "$0" "Add group to mixed flush batch" -"$family" route add
table 111 "$group_prefix" nhid "$group" metric 30
+ ts_ip "$0" "Add ordinary route to mixed flush batch" -"$family"
route add table 111 "$plain_prefix" dev "$DEV" metric 40
+ ts_ip "$0" "Flush mixed batch with original dump display" -"$family"
-s -s route flush table 111
+ test_on "nhid $nh2"
+ test_on "nhid $group"
+ ts_ip "$0" "Check no selected routes remain" -"$family" route show table 111
+ test_lines_count 0
+ ts_ip "$0" "Flush second table" -"$family" route flush table 112
+done
+
+# A blackhole object has no compatibility gateway/device fields to strip.
+ts_ip "$0" "Create IPv4 blackhole nexthop" -4 nexthop add id 301 blackhole
+ts_ip "$0" "Add blackhole-object route" -4 route add table 111
192.0.2.0/24 nhid 301
+ts_ip "$0" "Flush ID-only blackhole route" -4 route flush table 111
+ts_ip "$0" "Check blackhole object retained" nexthop show id 301
+test_on "id 301 blackhole"
+for type in blackhole unreachable prohibit; do
+ ts_ip "$0" "Add $type alias using blackhole object" -4 route add
"$type" 192.0.2.0/24 table 111 nhid 301
+ ts_ip "$0" "Flush $type alias without relying on synthesized dump
type" -4 route flush table 111
+ ts_ip "$0" "Check $type alias removed" -4 route show table 111
+ test_lines_count 0
+done
+ts_ip "$0" "Create IPv6 blackhole nexthop" -6 nexthop add id 302 blackhole
+ts_ip "$0" "Add IPv6 blackhole-object route" -6 route add table 111
2001:db8:5::/64 nhid 302
+ts_ip "$0" "Flush IPv6 ID-only blackhole route" -6 route flush table 111
+ts_ip "$0" "Check IPv6 blackhole object retained" nexthop show id 302
+test_on "id 302 blackhole"
+ts_ip "$0" "Add ordinary blackhole route without ID" -4 route add
blackhole 192.0.2.0/24 table 111
+ts_ip "$0" "Flush ordinary blackhole route" -4 route flush table 111
+ts_ip "$0" "Check ordinary blackhole route removed" -4 route show table 111
+test_lines_count 0
+
+# Type filtering must not delete another alias sharing the prefix,
metric and ID.
+ts_ip "$0" "Add unicast type-filter sentinel" -4 route add
203.0.113.0/24 table 111 nhid 101 metric 50
+ts_ip "$0" "Append explicit blackhole alias with same nexthop" -4
route append blackhole 203.0.113.0/24 table 111 nhid 101 metric 50
+ts_ip "$0" "Flush only explicit blackhole type" -4 route flush table
111 type blackhole
+ts_ip "$0" "Check unicast alias survived type filtering" -4 route
show table 111
+test_on "nhid 101"
+test_lines_count 1
+ts_ip "$0" "Delete remaining unicast sentinel by exact type" -4 route
del unicast 203.0.113.0/24 table 111 nhid 101 metric 50
+
+# A blackhole object hides the original IPv4 alias type in dumps. An explicit
+# type request must fail conservatively, rather than delete the unicast alias.
+ts_ip "$0" "Add alias with synthesized dump type" -4 route add
192.0.2.0/24 table 111 nhid 301
+if $IP -4 route flush table 111 type blackhole > "$STD_OUT" 2> "$STD_ERR"; then
+ ts_err "Ambiguous blackhole-object type filter unexpectedly succeeded"
+else
+ ts_log "Ambiguous explicit type failed conservatively:"
+ ts_cat "$STD_ERR"
+fi
+ts_ip "$0" "Check ambiguous unicast alias retained" -4 route show table 111
+test_lines_count 1
+ts_ip "$0" "Delete retained alias by its actual unicast type" -4
route del unicast 192.0.2.0/24 table 111 nhid 301
+
+# IPv4 via an IPv6 nexthop expands to RTA_VIA rather than RTA_GATEWAY.
+ts_ip "$0" "Add cross-family nexthop route" -4 route add table 111
203.0.113.0/24 nhid 201
+ts_ip "$0" "Flush cross-family route" -4 route flush table 111
+ts_ip "$0" "Check cross-family route removed" -4 route show table 111
+test_lines_count 0
+
+ts_ip "$0" "Delete test device and its nexthops" link del "$DEV"
+ts_ip "$0" "Delete blackhole nexthop" nexthop del id 301
+ts_ip "$0" "Delete IPv6 blackhole nexthop" nexthop del id 302
--
2.53.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH iproute2] ip: flush nexthop-object routes without compatibility attributes
2026-10-18 3:00 [PATCH iproute2] ip: flush nexthop-object routes without compatibility attributes Yuen Su
@ 2026-10-25 17:47 ` Stephen Hemminger
0 siblings, 0 replies; 2+ messages in thread
From: Stephen Hemminger @ 2026-10-25 17:47 UTC (permalink / raw)
To: Yuen Su; +Cc: netdev
On Sat, 17 Oct 2026 20:00:26 -0700
Yuen Su <zypulsecore@gmail.com> wrote:
> Route dumps carry RTA_NH_ID together with compatibility-expanded nexthop
> attributes. Copying both into RTM_DELROUTE is rejected. Compact the
> flush copy while preserving the exact ID and route selectors, use its
> new length for batching, and leave the original dump intact for verbose
> display.
>
> IPv4 blackhole objects synthesize a route type which may differ from the
> FIB alias. Without a type selector, omit that synthetic constraint.
> Retain explicit type filters so a typed flush cannot delete another
> alias sharing the prefix, metric and nexthop ID. An ambiguous typed
> request remains a conservative failure.
>
> Add real-kernel regressions for IPv4/IPv6 single nexthops, groups,
> ordinary multipath, mixed flush batches, blackhole aliases, cross-family
> gateways, exact metric/table retention and type-filter preservation.
>
> Signed-off-by: Yuen Su <zypulsecore@gmail.com>
> ---
Lots of issues identified by AI review
On Sat, 17 Oct 2026 20:00:26 -0700, Yuen Su wrote:
> Subject: [PATCH iproute2] ip: flush nexthop-object routes without
> compatibility attributes
The underlying bug is real and worth fixing, but this submission has a
blocking delivery problem, one correctness regression, and some invented
test scaffolding. Details below.
1) The patch is whitespace-damaged and cannot be applied
---------------------------------------------------------
$ git apply --check iproute2-ip-flush-nexthop-object-routes...patch
error: corrupt patch at line 192
The mail was injected through the Gmail web API:
> Received: from 77377267392 named unknown by gmailapi.google.com with HTTPREST;
which rewrapped long lines mid-statement. For example the new function's
signature arrives split across a continuation line, and nearly every
added test line is broken at column ~70:
> +static void flush_nexthop_compat(struct nlmsghdr *n,
> + const struct nlmsghdr *source)
> + ts_ip "$0" "Add selected single-nexthop route" -"$family" route add
> table 111 "$prefix" nhid "$nh" metric 10 mtu 1400 advmss 1360
Please resend with git send-email (or git format-patch + an attachment)
so it applies cleanly. I reviewed the logic from the quoted text, but
nothing can be tested in this state.
2) The RTN_UNSPEC hunk papers over a kernel dump bug -- please call
that out explicitly
--------------------------------------------------------------------
> + if (!filter.typemask &&
> + r->rtm_family == AF_INET && r->rtm_type == RTN_BLACKHOLE)
> + flushed->rtm_type = RTN_UNSPEC;
The commit message describes this as IPv4 "synthesizing a route type",
which undersells it. In fib_dump_info() (net/ipv4/fib_semantics.c):
if (fi->nh) {
if (nla_put_u32(skb, RTA_NH_ID, fi->nh->id))
goto nla_put_failure;
if (nexthop_is_blackhole(fi->nh))
rtm->rtm_type = RTN_BLACKHOLE;
if (!READ_ONCE(fi->fib_net->ipv4.sysctl_nexthop_compat_mode))
goto offload;
}
Note the type is overwritten *before* the compat-mode bail-out. So for
ip nexthop add id 301 blackhole
ip route add 192.0.2.0/24 nhid 301 # fa_type = RTN_UNICAST
the dump reports RTN_BLACKHOLE regardless of nexthop_compat_mode. A
delete built from that dump carries fc_type = RTN_BLACKHOLE, fails to
match fa_type = RTN_UNICAST in fib_table_delete(), returns ESRCH and
aborts the flush. That is a kernel bug: rtm_type ends up describing the
nexthop object rather than the FIB alias, and it is independent of the
compatibility-attribute problem this patch is otherwise fixing.
I am not asking you to drop the hunk -- without it, ID-only flush of
IPv4 blackhole-object routes stays broken, including your own test case.
But two things should change.
First, the commit message must say plainly that this works around a
kernel dump bug, with the trace above, rather than calling it a
"synthetic constraint". Otherwise the workaround outlives the memory of
why it exists. Please also report it on netdev so the dump can be fixed
at the source and this can eventually be reverted.
Second, be accurate about the cost in the comment. fc_type == 0 is a
wildcard in fib_table_delete() (net/ipv4/fib_trie.c), and the loop takes
the first otherwise-matching alias and stops:
if ((!cfg->fc_type || fa->fa_type == cfg->fc_type) &&
...
fa_to_delete = fa;
break;
The delete still carries RTA_NH_ID, protocol, scope, prefsrc and
metrics, all matched here and in fib_nh_match(), so the exposure is
narrow: only aliases identical in prefix, table, dscp, metric, nexthop
ID, protocol, scope and prefsrc, differing *only* in type. And since
this path requires !filter.typemask, the flush dumps and deletes all of
them in the same pass anyway -- so in the steady state this is an
order swap, not an over-deletion. Residual risk is a concurrent-change
window, not the general case.
That is a defensible tradeoff; it just needs to be the documented one.
The current comment ("Never weaken an explicit type filter") describes
the guard but not what is given up when the guard does not apply.
3) Invented test environment variable
--------------------------------------
> +if [ -n "$IPROUTE_TEST_PARENT_NETNS" ] &&
> + [ "$(readlink /proc/self/ns/net)" = "$IPROUTE_TEST_PARENT_NETNS" ]; then
> + ts_err "Refusing route writes in the parent network namespace"
> + exit 1
> +fi
IPROUTE_TEST_PARENT_NETNS does not exist anywhere in the tree:
$ grep -rn IPROUTE_TEST_PARENT_NETNS .
(no hits)
Nothing sets it, so the guard is permanently false -- dead code. The
testsuite already isolates every test; testsuite/Makefile has:
PREFIX := sudo -E unshare -n
Please drop the block.
4) ts_ip treats any stderr output as a failure
-----------------------------------------------
Worth double-checking the new test against __ts_cmd() in
testsuite/lib/generic.sh:
$CMD $@ 2> $STD_ERR > $STD_OUT
if [ -s $STD_ERR ]; then
ts_err "${SCRIPT}: ${DESC} failed:"
Any warning on stderr fails the step regardless of exit status. The
deliberate-failure case correctly bypasses ts_ip and invokes $IP
directly, so that one is fine -- but several of the nexthop and route
add steps will emit warnings on kernels without full nexthop-object
support and fail for the wrong reason. Consider gating the whole file
on a nexthop-object probe with ts_skip.
5) What is right, for the record
---------------------------------
The core of the fix is correct and I'd like to see it respun:
- The strip set matches what the kernel actually rejects, for both
families. net/ipv4/fib_frontend.c:
if (cfg->fc_nh_id) {
if (cfg->fc_oif || cfg->fc_gw_family ||
cfg->fc_encap || cfg->fc_mp) {
NL_SET_ERR_MSG(extack,
"Nexthop specification and nexthop id are mutually exclusive");
and net/ipv6/route.c rejects RTA_GATEWAY/RTA_OIF/RTA_MULTIPATH/RTA_ENCAP
alongside RTA_NH_ID. Since nexthop_compat_mode defaults to 1, the dump
does carry both, so flush really is broken today.
- The buffer accounting is sound. The overflow check still uses the
original (larger) n->nlmsg_len, so it stays conservative, and
> - filter.flushp = (((char *)fn) + n->nlmsg_len) - filter.flushb;
> + filter.flushp = (((char *)fn) + fn->nlmsg_len) - filter.flushb;
is load-bearing: without it the next message in the batch would be
written after a gap and the kernel would parse garbage. Reading from
the source message while writing into the flush buffer is safe --
they are distinct buffers.
6) Minor
---------
- The commit message should state the problem, the root cause and the
fix. The validation appendix (CI links, assertion counts, sanitizer
claims, "No production router or NSS firmware was tested") does not
belong in the posting; trim it to the one-line note that real-kernel
regression tests were added.
- Naming: the local `flushed` reads like the filter.flushed counter.
Something like `dr` (delete request) would be clearer.
So: fix the delivery, document the RTN_UNSPEC workaround for what it is
(and report the dump bug upstream), drop the dead netns guard, and the
rest looks like the right fix.
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-25 17:47 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-18 3:00 [PATCH iproute2] ip: flush nexthop-object routes without compatibility attributes Yuen Su
2026-10-25 17:47 ` Stephen Hemminger
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox