AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/08 01:20 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "IPV6",
    "IPV6_ROUTER_PREF",
    "IPV6_ROUTE_INFO"
  ],
  "FocusSymbols": [
    "ip6_del_rt_reason",
    "inet6_rt_del_notify",
    "fib6_del"
  ],
  "KMSANReasoning": "The patch introduces a new netlink attribute `RTA_DEL_REASON` to report the reason for IPv6 route deletions. The deletion reason is passed through the routing subsystem as an explicit `enum rt_del_reason` value (e.g., `RT_DEL_REASON_UNSPEC`, `RT_DEL_REASON_EXPIRED`, `RT_DEL_REASON_RA_WITHDRAWN`). This value is then serialized into the netlink message using `nla_put_u32()`. \n\nSince the `del_reason` is passed by value as an explicit constant in all modified call sites, there is no risk of reading uninitialized memory from stack, heap, or padding bytes. The netlink attribute construction via `nla_put_u32()` safely handles the serialization without exposing uninitialized kernel memory to user space. Therefore, standard KASAN and other bug detectors are sufficient to catch any potential memory safety issues (like out-of-bounds accesses or use-after-free) in this code, and a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch introduces a new netlink attribute RTA_DEL_REASON for IPv6 route deletion notifications, modifying core IPv6 routing and netlink message construction logic. This is a functional change that is reachable via route expiration, router advertisements, and manual route deletion, and should be fuzzed.",
  "WorthFuzzing": true
}

1/1 2026/08/08 01:20 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 7b445bd02f4f88497ebdad918798b8c83d196a91\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Sat Aug 8 01:20:13 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/Documentation/netlink/specs/rt-route.yaml b/Documentation/netlink/specs/rt-route.yaml\nindex 33195db96746c..253037ea5176a 100644\n--- a/Documentation/netlink/specs/rt-route.yaml\n+++ b/Documentation/netlink/specs/rt-route.yaml\n@@ -78,6 +78,27 @@ definitions:\n       -\n         name: rta-used\n         type: u32\n+  -\n+    name: del-reason\n+    type: enum\n+    name-prefix: rt-del-reason-\n+    enum-name: rt-del-reason\n+    doc: |\n+      Why the kernel deleted a route. New causes may be appended. The\n+      value space is family-agnostic, a value is never reinterpreted\n+      per address family.\n+    entries:\n+      -\n+        name: unspec\n+        doc: The deletion path does not record a cause.\n+      -\n+        name: expired\n+        doc: The RTF_EXPIRES lifetime ran out and the route was garbage\n+          collected.\n+      -\n+        name: ra-withdrawn\n+        doc: A Router Advertisement withdrew the route with a zero\n+          lifetime.\n \n attribute-sets:\n   -\n@@ -185,6 +206,18 @@ attribute-sets:\n         type: u32\n         byte-order: big-endian\n         display-hint: hex\n+      -\n+        name: del-reason\n+        type: u32\n+        enum: del-reason\n+        doc: |\n+          Emitted only on RTM_DELROUTE notifications, and only when the\n+          deletion path records a cause. An absent attribute means\n+          either an older kernel or a deletion path that does not\n+          record its cause, consumers must treat absent and unspec\n+          identically. Currently only IPv6 deletion paths record a\n+          cause. The attribute is notification-only, the kernel rejects\n+          it in requests.\n   -\n     name: metrics\n     name-prefix: rtax-\n@@ -299,6 +332,7 @@ operations:\n             - dport\n             - nh-id\n             - flowlabel\n+            - del-reason\n       dump:\n         request:\n           value: 26\n@@ -313,7 +347,35 @@ operations:\n       do:\n         request:\n           value: 24\n-          attributes: *all-route-attrs\n+          attributes: \u0026route-req-attrs\n+            - dst\n+            - src\n+            - iif\n+            - oif\n+            - gateway\n+            - priority\n+            - prefsrc\n+            - metrics\n+            - multipath\n+            - flow\n+            - cacheinfo\n+            - table\n+            - mark\n+            - mfc-stats\n+            - via\n+            - newdst\n+            - pref\n+            - encap-type\n+            - encap\n+            - expires\n+            - pad\n+            - uid\n+            - ttl-propagate\n+            - ip-proto\n+            - sport\n+            - dport\n+            - nh-id\n+            - flowlabel\n     -\n       name: delroute\n       doc: Delete an existing route\n@@ -321,4 +383,23 @@ operations:\n       do:\n         request:\n           value: 25\n-          attributes: *all-route-attrs\n+          attributes: *route-req-attrs\n+    -\n+      name: newroute-ntf\n+      doc: Notification about a created route.\n+      value: 24\n+      notify: getroute\n+    -\n+      name: delroute-ntf\n+      doc: Notification about a deleted route.\n+      value: 25\n+      notify: getroute\n+\n+mcast-groups:\n+  list:\n+    -\n+      name: rtnlgrp-ipv4-route\n+      value: 7\n+    -\n+      name: rtnlgrp-ipv6-route\n+      value: 11\ndiff --git a/include/net/ip6_fib.h b/include/net/ip6_fib.h\nindex 9cd27e1b9b692..232289b439c3c 100644\n--- a/include/net/ip6_fib.h\n+++ b/include/net/ip6_fib.h\n@@ -468,7 +468,8 @@ void fib6_clean_all_skip_notify(struct net *net,\n \n int fib6_add(struct fib6_node *root, struct fib6_info *rt,\n \t     struct nl_info *info, struct netlink_ext_ack *extack);\n-int fib6_del(struct fib6_info *rt, struct nl_info *info);\n+int fib6_del(struct fib6_info *rt, struct nl_info *info,\n+\t     enum rt_del_reason del_reason);\n \n static inline\n void rt6_get_prefsrc(const struct rt6_info *rt, struct in6_addr *addr)\n@@ -532,6 +533,8 @@ static inline void fib6_rt_update(struct net *net, struct fib6_info *rt,\n #endif\n void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,\n \t\t     unsigned int flags);\n+void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info,\n+\t\t\t enum rt_del_reason del_reason);\n \n void fib6_age_exceptions(struct fib6_info *rt, struct fib6_gc_args *gc_args,\n \t\t\t unsigned long now);\ndiff --git a/include/net/ip6_route.h b/include/net/ip6_route.h\nindex 09ffe0f13ce75..cc045705862de 100644\n--- a/include/net/ip6_route.h\n+++ b/include/net/ip6_route.h\n@@ -126,6 +126,8 @@ int ipv6_route_ioctl(struct net *net, unsigned int cmd,\n int ip6_route_add(struct fib6_config *cfg, gfp_t gfp_flags,\n \t\t  struct netlink_ext_ack *extack);\n int ip6_ins_rt(struct net *net, struct fib6_info *f6i);\n+int ip6_del_rt_reason(struct net *net, struct fib6_info *f6i,\n+\t\t      enum rt_del_reason del_reason);\n #if IS_ENABLED(CONFIG_IPV6)\n int ip6_del_rt(struct net *net, struct fib6_info *f6i, bool skip_notify);\n #else\ndiff --git a/include/uapi/linux/rtnetlink.h b/include/uapi/linux/rtnetlink.h\nindex 27265fd31e5fc..3988b51db539f 100644\n--- a/include/uapi/linux/rtnetlink.h\n+++ b/include/uapi/linux/rtnetlink.h\n@@ -399,6 +399,7 @@ enum rtattr_type_t {\n \tRTA_DPORT,\n \tRTA_NH_ID,\n \tRTA_FLOWLABEL,\n+\tRTA_DEL_REASON,\n \t__RTA_MAX\n };\n \n@@ -407,6 +408,30 @@ enum rtattr_type_t {\n #define RTM_RTA(r)  ((struct rtattr*)(((char*)(r)) + NLMSG_ALIGN(sizeof(struct rtmsg))))\n #define RTM_PAYLOAD(n) NLMSG_PAYLOAD(n,sizeof(struct rtmsg))\n \n+/* RTA_DEL_REASON: why the kernel deleted the route. u32.\n+ * Emitted only on RTM_DELROUTE notifications, and only when the deletion\n+ * path records a cause. Absence means either an older kernel or a\n+ * deletion path that does not (yet) record its cause - consumers must\n+ * treat \"absent\" and \"unspec\" identically. New causes may be appended.\n+ * Currently only IPv6 deletion paths record a cause.\n+ *\n+ * The attribute is notification-only: the kernel rejects it in\n+ * requests, so a notification must not be echoed back verbatim.\n+ *\n+ * The value space is family-agnostic: a value must never be\n+ * reinterpreted per address family. A cause that only one family can\n+ * produce still gets its own value rather than reusing another\n+ * family's.\n+ */\n+enum rt_del_reason {\n+\tRT_DEL_REASON_UNSPEC,\t\t/* cause not recorded */\n+\tRT_DEL_REASON_EXPIRED,\t\t/* RTF_EXPIRES lifetime ran out (GC) */\n+\tRT_DEL_REASON_RA_WITHDRAWN,\t/* zero-lifetime RA / PIO / RIO */\n+\t__RT_DEL_REASON_MAX\n+};\n+\n+#define RT_DEL_REASON_MAX (__RT_DEL_REASON_MAX - 1)\n+\n /* RTM_MULTIPATH --- array of struct rtnexthop.\n  *\n  * \"struct rtnexthop\" describes all necessary nexthop information,\ndiff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c\nindex f6fa2715b450b..9d89be7e0544b 100644\n--- a/net/ipv6/addrconf.c\n+++ b/net/ipv6/addrconf.c\n@@ -2874,7 +2874,8 @@ void addrconf_prefix_rcv(struct net_device *dev, u8 *opt, int len, bool sllao)\n \t\tif (rt) {\n \t\t\t/* Autoconf prefix route */\n \t\t\tif (valid_lft == 0) {\n-\t\t\t\tip6_del_rt(net, rt, false);\n+\t\t\t\tip6_del_rt_reason(net, rt,\n+\t\t\t\t\t\t  RT_DEL_REASON_RA_WITHDRAWN);\n \t\t\t\trt = NULL;\n \t\t\t} else {\n \t\t\t\ttable = rt-\u003efib6_table;\ndiff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c\nindex e9fc692d4f3b2..3e382ba1573e3 100644\n--- a/net/ipv6/ip6_fib.c\n+++ b/net/ipv6/ip6_fib.c\n@@ -1967,7 +1967,8 @@ static struct fib6_node *fib6_repair_tree(struct net *net,\n }\n \n static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn,\n-\t\t\t   struct fib6_info __rcu **rtp, struct nl_info *info)\n+\t\t\t   struct fib6_info __rcu **rtp, struct nl_info *info,\n+\t\t\t   enum rt_del_reason del_reason)\n {\n \tstruct fib6_info *leaf, *replace_rt = NULL;\n \tstruct fib6_walker *w;\n@@ -2056,13 +2057,14 @@ static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn,\n \t\t\tcall_fib6_entry_notifiers_replace(net, replace_rt);\n \t}\n \tif (!info-\u003eskip_notify)\n-\t\tinet6_rt_notify(RTM_DELROUTE, rt, info, 0);\n+\t\tinet6_rt_del_notify(rt, info, del_reason);\n \n \tfib6_info_release(rt);\n }\n \n /* Need to own table-\u003etb6_lock */\n-int fib6_del(struct fib6_info *rt, struct nl_info *info)\n+int fib6_del(struct fib6_info *rt, struct nl_info *info,\n+\t     enum rt_del_reason del_reason)\n {\n \tstruct net *net = info-\u003enl_net;\n \tstruct fib6_info __rcu **rtp;\n@@ -2091,7 +2093,7 @@ int fib6_del(struct fib6_info *rt, struct nl_info *info)\n \t\tif (rt == cur) {\n \t\t\tif (fib6_requires_src(cur))\n \t\t\t\tfib6_routes_require_src_dec(info-\u003enl_net);\n-\t\t\tfib6_del_route(table, fn, rtp, info);\n+\t\t\tfib6_del_route(table, fn, rtp, info, del_reason);\n \t\t\treturn 0;\n \t\t}\n \t\trtp_next = \u0026cur-\u003efib6_next;\n@@ -2253,7 +2255,7 @@ static int fib6_clean_node(struct fib6_walker *w)\n \t\tres = c-\u003efunc(rt, c-\u003earg);\n \t\tif (res == -1) {\n \t\t\tw-\u003eleaf = rt;\n-\t\t\tres = fib6_del(rt, \u0026info);\n+\t\t\tres = fib6_del(rt, \u0026info, RT_DEL_REASON_UNSPEC);\n \t\t\tif (res) {\n #if RT6_DEBUG \u003e= 2\n \t\t\t\tpr_debug(\"%s: del failed: rt=%p@%p err=%d\\n\",\n@@ -2401,7 +2403,7 @@ static void fib6_gc_table(struct net *net,\n \n \thlist_for_each_entry_safe(rt, n, \u0026tb6-\u003etb6_gc_hlist, gc_link)\n \t\tif (fib6_age(rt, gc_args) == -1)\n-\t\t\tfib6_del(rt, \u0026info);\n+\t\t\tfib6_del(rt, \u0026info, RT_DEL_REASON_EXPIRED);\n }\n \n static void fib6_gc_all(struct net *net, struct fib6_gc_args *gc_args)\ndiff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c\nindex fe36b3f512850..1111c4ebc61d9 100644\n--- a/net/ipv6/ndisc.c\n+++ b/net/ipv6/ndisc.c\n@@ -1361,7 +1361,9 @@ static enum skb_drop_reason ndisc_router_discovery(struct sk_buff *skb)\n \tdefrtr_usr_metric = in6_dev-\u003ecnf.ra_defrtr_metric;\n \t/* delete the route if lifetime is 0 or if metric needs change */\n \tif (rt \u0026\u0026 (lifetime == 0 || rt-\u003efib6_metric != defrtr_usr_metric)) {\n-\t\tip6_del_rt(net, rt, false);\n+\t\tip6_del_rt_reason(net, rt,\n+\t\t\t\t  lifetime == 0 ? RT_DEL_REASON_RA_WITHDRAWN :\n+\t\t\t\t\t\t  RT_DEL_REASON_UNSPEC);\n \t\trt = NULL;\n \t}\n \ndiff --git a/net/ipv6/route.c b/net/ipv6/route.c\nindex fc42d67e58226..1704359d3960d 100644\n--- a/net/ipv6/route.c\n+++ b/net/ipv6/route.c\n@@ -111,7 +111,7 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,\n \t\t\t struct fib6_info *rt, struct dst_entry *dst,\n \t\t\t struct in6_addr *dest, struct in6_addr *src,\n \t\t\t int iif, int type, u32 portid, u32 seq,\n-\t\t\t unsigned int flags);\n+\t\t\t unsigned int flags, enum rt_del_reason del_reason);\n static struct rt6_info *rt6_find_cached_rt(const struct fib6_result *res,\n \t\t\t\t\t   const struct in6_addr *daddr,\n \t\t\t\t\t   const struct in6_addr *saddr);\n@@ -1020,7 +1020,7 @@ int rt6_route_rcv(struct net_device *dev, u8 *opt, int len,\n \t\t\t\t\tgwaddr, dev);\n \n \tif (rt \u0026\u0026 !lifetime) {\n-\t\tip6_del_rt(net, rt, false);\n+\t\tip6_del_rt_reason(net, rt, RT_DEL_REASON_RA_WITHDRAWN);\n \t\trt = NULL;\n \t}\n \n@@ -3973,7 +3973,8 @@ int ip6_route_add(struct fib6_config *cfg, gfp_t gfp_flags,\n \treturn err;\n }\n \n-static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info)\n+static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info,\n+\t\t\tenum rt_del_reason del_reason)\n {\n \tstruct net *net = info-\u003enl_net;\n \tstruct fib6_table *table;\n@@ -3986,7 +3987,7 @@ static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info)\n \n \ttable = rt-\u003efib6_table;\n \tspin_lock_bh(\u0026table-\u003etb6_lock);\n-\terr = fib6_del(rt, info);\n+\terr = fib6_del(rt, info, del_reason);\n \tspin_unlock_bh(\u0026table-\u003etb6_lock);\n \n out:\n@@ -4001,7 +4002,15 @@ int ip6_del_rt(struct net *net, struct fib6_info *rt, bool skip_notify)\n \t\t.skip_notify = skip_notify\n \t};\n \n-\treturn __ip6_del_rt(rt, \u0026info);\n+\treturn __ip6_del_rt(rt, \u0026info, RT_DEL_REASON_UNSPEC);\n+}\n+\n+int ip6_del_rt_reason(struct net *net, struct fib6_info *rt,\n+\t\t      enum rt_del_reason del_reason)\n+{\n+\tstruct nl_info info = { .nl_net = net };\n+\n+\treturn __ip6_del_rt(rt, \u0026info, del_reason);\n }\n \n static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)\n@@ -4028,7 +4037,8 @@ static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)\n \n \t\t\tif (rt6_fill_node(net, skb, rt, NULL,\n \t\t\t\t\t  NULL, NULL, 0, RTM_DELROUTE,\n-\t\t\t\t\t  info-\u003eportid, seq, 0) \u003c 0) {\n+\t\t\t\t\t  info-\u003eportid, seq, 0,\n+\t\t\t\t\t  RT_DEL_REASON_UNSPEC) \u003c 0) {\n \t\t\t\tkfree_skb(skb);\n \t\t\t\tskb = NULL;\n \t\t\t} else\n@@ -4064,13 +4074,13 @@ static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)\n \t\tlist_for_each_entry_safe(sibling, next_sibling,\n \t\t\t\t\t \u0026rt-\u003efib6_siblings,\n \t\t\t\t\t fib6_siblings) {\n-\t\t\terr = fib6_del(sibling, info);\n+\t\t\terr = fib6_del(sibling, info, RT_DEL_REASON_UNSPEC);\n \t\t\tif (err)\n \t\t\t\tgoto out_unlock;\n \t\t}\n \t}\n \n-\terr = fib6_del(rt, info);\n+\terr = fib6_del(rt, info, RT_DEL_REASON_UNSPEC);\n out_unlock:\n \tspin_unlock_bh(\u0026table-\u003etb6_lock);\n out_put:\n@@ -4196,7 +4206,8 @@ static int ip6_route_del(struct fib6_config *cfg,\n \t\t\t\tif (!fib6_info_hold_safe(rt))\n \t\t\t\t\tcontinue;\n \n-\t\t\t\terr =  __ip6_del_rt(rt, \u0026cfg-\u003efc_nlinfo);\n+\t\t\t\terr = __ip6_del_rt(rt, \u0026cfg-\u003efc_nlinfo,\n+\t\t\t\t\t\t   RT_DEL_REASON_UNSPEC);\n \t\t\t\tbreak;\n \t\t\t}\n \t\t\tif (cfg-\u003efc_nh_id)\n@@ -4215,7 +4226,8 @@ static int ip6_route_del(struct fib6_config *cfg,\n \n \t\t\t/* if gateway was specified only delete the one hop */\n \t\t\tif (cfg-\u003efc_flags \u0026 RTF_GATEWAY)\n-\t\t\t\terr = __ip6_del_rt(rt, \u0026cfg-\u003efc_nlinfo);\n+\t\t\t\terr = __ip6_del_rt(rt, \u0026cfg-\u003efc_nlinfo,\n+\t\t\t\t\t\t   RT_DEL_REASON_UNSPEC);\n \t\t\telse\n \t\t\t\terr = __ip6_del_rt_siblings(rt, cfg);\n \t\t\tbreak;\n@@ -5739,6 +5751,7 @@ static size_t rt6_nlmsg_size(struct fib6_info *f6i)\n \t       + nla_total_size(sizeof(struct rta_cacheinfo))\n \t       + nla_total_size(TCP_CA_NAME_MAX) /* RTAX_CC_ALGO */\n \t       + nla_total_size(1) /* RTA_PREF */\n+\t       + nla_total_size(4) /* RTA_DEL_REASON */\n \t       + nexthop_len;\n }\n \n@@ -5775,7 +5788,7 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,\n \t\t\t struct fib6_info *rt, struct dst_entry *dst,\n \t\t\t struct in6_addr *dest, struct in6_addr *src,\n \t\t\t int iif, int type, u32 portid, u32 seq,\n-\t\t\t unsigned int flags)\n+\t\t\t unsigned int flags, enum rt_del_reason del_reason)\n {\n \tstruct rt6_info *rt6 = dst_rt6_info(dst);\n \tstruct rt6key *rt6_dst, *rt6_src;\n@@ -5954,6 +5967,10 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,\n \tif (rtnl_put_cacheinfo(skb, dst, 0, expires, dst ? dst-\u003eerror : 0) \u003c 0)\n \t\tgoto nla_put_failure;\n \n+\tif (del_reason != RT_DEL_REASON_UNSPEC \u0026\u0026\n+\t    nla_put_u32(skb, RTA_DEL_REASON, del_reason))\n+\t\tgoto nla_put_failure;\n+\n \tif (nla_put_u8(skb, RTA_PREF, IPV6_EXTRACT_PREF(rt6_flags)))\n \t\tgoto nla_put_failure;\n \n@@ -6055,7 +6072,8 @@ static int rt6_nh_dump_exceptions(struct fib6_nh *nh, void *arg)\n \t\t\t\t\t    \u0026rt6_ex-\u003ert6i-\u003edst, NULL, NULL, 0,\n \t\t\t\t\t    RTM_NEWROUTE,\n \t\t\t\t\t    NETLINK_CB(dump-\u003ecb-\u003eskb).portid,\n-\t\t\t\t\t    dump-\u003ecb-\u003enlh-\u003enlmsg_seq, w-\u003eflags);\n+\t\t\t\t\t    dump-\u003ecb-\u003enlh-\u003enlmsg_seq, w-\u003eflags,\n+\t\t\t\t\t    RT_DEL_REASON_UNSPEC);\n \t\t\tif (err)\n \t\t\t\treturn err;\n \n@@ -6103,7 +6121,8 @@ int rt6_dump_route(struct fib6_info *rt, void *p_arg, unsigned int skip)\n \t\t\tif (rt6_fill_node(net, arg-\u003eskb, rt, NULL, NULL, NULL,\n \t\t\t\t\t  0, RTM_NEWROUTE,\n \t\t\t\t\t  NETLINK_CB(arg-\u003ecb-\u003eskb).portid,\n-\t\t\t\t\t  arg-\u003ecb-\u003enlh-\u003enlmsg_seq, flags)) {\n+\t\t\t\t\t  arg-\u003ecb-\u003enlh-\u003enlmsg_seq, flags,\n+\t\t\t\t\t  RT_DEL_REASON_UNSPEC)) {\n \t\t\t\treturn 0;\n \t\t\t}\n \t\t\tcount++;\n@@ -6336,12 +6355,14 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,\n \t\t\terr = rt6_fill_node(net, skb, from, NULL, NULL, NULL,\n \t\t\t\t\t    iif, RTM_NEWROUTE,\n \t\t\t\t\t    NETLINK_CB(in_skb).portid,\n-\t\t\t\t\t    nlh-\u003enlmsg_seq, 0);\n+\t\t\t\t\t    nlh-\u003enlmsg_seq, 0,\n+\t\t\t\t\t    RT_DEL_REASON_UNSPEC);\n \t\telse\n \t\t\terr = rt6_fill_node(net, skb, from, dst, \u0026fl6.daddr,\n \t\t\t\t\t    \u0026fl6.saddr, iif, RTM_NEWROUTE,\n \t\t\t\t\t    NETLINK_CB(in_skb).portid,\n-\t\t\t\t\t    nlh-\u003enlmsg_seq, 0);\n+\t\t\t\t\t    nlh-\u003enlmsg_seq, 0,\n+\t\t\t\t\t    RT_DEL_REASON_UNSPEC);\n \t} else {\n \t\terr = -ENETUNREACH;\n \t}\n@@ -6357,8 +6378,9 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,\n \treturn err;\n }\n \n-void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,\n-\t\t     unsigned int nlm_flags)\n+static void __inet6_rt_notify(int event, struct fib6_info *rt,\n+\t\t\t      struct nl_info *info, unsigned int nlm_flags,\n+\t\t\t      enum rt_del_reason del_reason)\n {\n \tstruct net *net = info-\u003enl_net;\n \tstruct sk_buff *skb;\n@@ -6377,7 +6399,7 @@ void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,\n \t\tgoto errout;\n \n \terr = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,\n-\t\t\t    event, info-\u003eportid, seq, nlm_flags);\n+\t\t\t    event, info-\u003eportid, seq, nlm_flags, del_reason);\n \tif (err \u003c 0) {\n \t\tkfree_skb(skb);\n \t\t/* -EMSGSIZE implies needed space grew under us. */\n@@ -6398,6 +6420,18 @@ void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,\n \trtnl_set_sk_err(net, RTNLGRP_IPV6_ROUTE, err);\n }\n \n+void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,\n+\t\t     unsigned int nlm_flags)\n+{\n+\t__inet6_rt_notify(event, rt, info, nlm_flags, RT_DEL_REASON_UNSPEC);\n+}\n+\n+void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info,\n+\t\t\t enum rt_del_reason del_reason)\n+{\n+\t__inet6_rt_notify(RTM_DELROUTE, rt, info, 0, del_reason);\n+}\n+\n void fib6_rt_update(struct net *net, struct fib6_info *rt,\n \t\t    struct nl_info *info)\n {\n@@ -6410,7 +6444,8 @@ void fib6_rt_update(struct net *net, struct fib6_info *rt,\n \t\tgoto errout;\n \n \terr = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,\n-\t\t\t    RTM_NEWROUTE, info-\u003eportid, seq, NLM_F_REPLACE);\n+\t\t\t    RTM_NEWROUTE, info-\u003eportid, seq, NLM_F_REPLACE,\n+\t\t\t    RT_DEL_REASON_UNSPEC);\n \tif (err \u003c 0) {\n \t\t/* -EMSGSIZE implies BUG in rt6_nlmsg_size() */\n \t\tWARN_ON(err == -EMSGSIZE);\n@@ -6463,7 +6498,7 @@ void fib6_info_hw_flags_set(struct net *net, struct fib6_info *f6i,\n \t}\n \n \terr = rt6_fill_node(net, skb, f6i, NULL, NULL, NULL, 0, RTM_NEWROUTE, 0,\n-\t\t\t    0, 0);\n+\t\t\t    0, 0, RT_DEL_REASON_UNSPEC);\n \tif (err \u003c 0) {\n \t\t/* -EMSGSIZE implies BUG in rt6_nlmsg_size() */\n \t\tWARN_ON(err == -EMSGSIZE);\ndiff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py\nindex e58bdbdc58eea..34935886b6adf 100644\n--- a/tools/testing/selftests/net/lib/py/__init__.py\n+++ b/tools/testing/selftests/net/lib/py/__init__.py\n@@ -17,7 +17,7 @@ from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \\\n     wait_file, tool, tc\n from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids\n from .ynl import NlError, NlctrlFamily, YnlFamily, \\\n-    EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily\n+    EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily\n from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink\n \n __all__ = [\"KSRC\",\n@@ -34,4 +34,4 @@ __all__ = [\"KSRC\",\n            \"NetdevSim\", \"NetdevSimDev\",\n            \"NetshaperFamily\", \"DevlinkFamily\", \"PSPFamily\", \"NlError\",\n            \"YnlFamily\", \"EthtoolFamily\", \"NetdevFamily\", \"RtnlFamily\",\n-           \"NlctrlFamily\", \"RtnlAddrFamily\", \"Netlink\"]\n+           \"NlctrlFamily\", \"RtnlAddrFamily\", \"RtnlRouteFamily\", \"Netlink\"]\ndiff --git a/tools/testing/selftests/net/lib/py/ynl.py b/tools/testing/selftests/net/lib/py/ynl.py\nindex 2e567062aa6cc..08deff756f29b 100644\n--- a/tools/testing/selftests/net/lib/py/ynl.py\n+++ b/tools/testing/selftests/net/lib/py/ynl.py\n@@ -29,7 +29,7 @@ except ModuleNotFoundError as e:\n \n __all__ = [\n     \"NlError\", \"NlPolicy\", \"Netlink\", \"YnlFamily\", \"SPEC_PATH\",\n-    \"EthtoolFamily\", \"RtnlFamily\", \"RtnlAddrFamily\",\n+    \"EthtoolFamily\", \"RtnlFamily\", \"RtnlAddrFamily\", \"RtnlRouteFamily\",\n     \"NetdevFamily\", \"NetshaperFamily\", \"NlctrlFamily\", \"DevlinkFamily\",\n     \"PSPFamily\",\n ]\n@@ -54,6 +54,11 @@ class RtnlAddrFamily(YnlFamily):\n         super().__init__((SPEC_PATH / Path('rt-addr.yaml')).as_posix(),\n                          schema='', recv_size=recv_size)\n \n+class RtnlRouteFamily(YnlFamily):\n+    def __init__(self, recv_size=0):\n+        super().__init__((SPEC_PATH / Path('rt-route.yaml')).as_posix(),\n+                         schema='', recv_size=recv_size)\n+\n class NetdevFamily(YnlFamily):\n     def __init__(self, recv_size=0):\n         super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(),\ndiff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py\nindex 0c67c7c00d841..5cc3ebdcf08de 100755\n--- a/tools/testing/selftests/net/rtnetlink.py\n+++ b/tools/testing/selftests/net/rtnetlink.py\n@@ -5,7 +5,9 @@ import socket\n import struct\n import time\n from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx\n-from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily\n+from lib.py import ksft_not_in, ksft_not_none\n+from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily, RtnlRouteFamily\n+from lib.py import defer\n \n IPV4_ALL_HOSTS_MULTICAST = b'\\xe0\\x00\\x00\\x01'\n IPV4_TEST_MULTICAST = b'\\xef\\x01\\x01\\x01'\n@@ -134,8 +136,189 @@ def ipv4_devconf_notify() -\u003e None:\n     ksft_true(f\"inet {ifname} forwarding on\" in cmd_obj.stdout,\n               f\"No 'forwarding on' notificiation found for interface {ifname}\")\n \n+def _rtnl_route_subscribe(ns):\n+    with NetNSEnter(str(ns)):\n+        rtnl = RtnlRouteFamily()\n+    defer(rtnl.close)\n+    rtnl.ntf_subscribe(\"rtnlgrp-ipv6-route\")\n+    return rtnl\n+\n+\n+def _wait_route_ntf(rtnl, name, dst_len, dst=None, deadline=10):\n+    \"\"\"Return the attrs of the first matching notification, None on timeout.\"\"\"\n+\n+    for msg in rtnl.poll_ntf(duration=deadline):\n+        if msg['name'] != name:\n+            continue\n+        attrs = msg['msg']\n+        if attrs['rtm-dst-len'] != dst_len:\n+            continue\n+        if dst is not None and attrs.get('dst') != dst:\n+            continue\n+        return attrs\n+    return None\n+\n+\n+def _collect_route_ntfs(rtnl, name, want, deadline=10):\n+    \"\"\"Gather attrs of matching notifications, keyed by (dst_len, dst).\"\"\"\n+\n+    seen = {}\n+    for msg in rtnl.poll_ntf(duration=deadline):\n+        if msg['name'] != name:\n+            continue\n+        attrs = msg['msg']\n+        key = (attrs['rtm-dst-len'], attrs.get('dst'))\n+        if key in want:\n+            seen[key] = attrs\n+            if len(seen) == len(want):\n+                break\n+    return seen\n+\n+\n+def _write_ipv6_sysctl(name, value):\n+    with open(f\"/proc/sys/net/ipv6/{name}\", \"w\") as f:\n+        f.write(f\"{value}\\n\")\n+\n+\n+def ipv6_route_del_reason_expired() -\u003e None:\n+    \"\"\"An expired route reports RTA_DEL_REASON == expired.\"\"\"\n+\n+    with NetNS() as ns:\n+        rtnl = _rtnl_route_subscribe(ns)\n+        with NetNSEnter(str(ns)):\n+            _write_ipv6_sysctl(\"route/gc_interval\", 2)\n+        ip(\"link add name dummy1 type dummy\", ns=str(ns))\n+        ip(\"link set dev dummy1 up\", ns=str(ns))\n+        ip(\"-6 route add 2001:db8:2::/64 dev dummy1 expires 2\", ns=str(ns))\n+\n+        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:2::',\n+                                deadline=15)\n+        ksft_not_none(attrs, \"no RTM_DELROUTE for the expired route\")\n+        if attrs is not None:\n+            ksft_eq(attrs.get('del-reason'), 'expired')\n+\n+\n+def _send_ra(sock, ifindex, lifetime, rio=None, pio=None):\n+    \"\"\"The kernel fills in the ICMPv6 checksum on raw ICMPv6 sockets.\"\"\"\n+\n+    # type, code, cksum, hop limit, flags, router lifetime,\n+    # reachable time, retrans timer\n+    ra = struct.pack('!BBHBBHII', 134, 0, 0, 64, 0, lifetime, 0, 0)\n+    if rio is not None:\n+        prefix, plen, rio_lifetime = rio\n+        # RFC 4191 route information option, /64 prefix (8 bytes)\n+        ra += struct.pack('!BBBBI', 24, 2, plen, 0, rio_lifetime)\n+        ra += socket.inet_pton(socket.AF_INET6, prefix)[:8]\n+    if pio is not None:\n+        prefix, plen, valid_lft = pio\n+        # RFC 4861 prefix information option, on-link only (L set, A clear)\n+        ra += struct.pack('!BBBBIII', 3, 4, plen, 0x80, valid_lft, 0, 0)\n+        ra += socket.inet_pton(socket.AF_INET6, prefix)\n+    sock.sendto(ra, ('ff02::1', 0, 0, ifindex))\n+\n+\n+def _ra_router_sock(ns_r, ifname):\n+    with NetNSEnter(str(ns_r)):\n+        sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW,\n+                             socket.IPPROTO_ICMPV6)\n+        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)\n+        defer(sock.close)\n+        return sock, socket.if_nametoindex(ifname)\n+\n+\n+def _ra_advertise_routes(rtnl, sock, ifindex, want, **ra_opts):\n+    \"\"\"\n+    Sending fails with EADDRNOTAVAIL while the router's link-local\n+    address is still tentative. addrconf_dad_start() only queues\n+    addrconf_dad_work(), and IFA_F_TENTATIVE is cleared when that work\n+    item runs, so retry until it does.\n+    \"\"\"\n+\n+    seen = {}\n+    for _ in range(10):\n+        try:\n+            _send_ra(sock, ifindex, **ra_opts)\n+        except OSError:\n+            time.sleep(0.2)\n+            continue\n+        seen.update(_collect_route_ntfs(rtnl, 'newroute-ntf',\n+                                        want - set(seen.keys()), deadline=2))\n+        if len(seen) == len(want):\n+            break\n+    return seen\n+\n+\n+def ipv6_route_del_reason_ra_withdrawn() -\u003e None:\n+    \"\"\"\n+    Routes withdrawn by a zero-lifetime RA (router lifetime, RFC 4861\n+    PIO, RFC 4191 RIO) report RTA_DEL_REASON == ra-withdrawn.\n+    \"\"\"\n+\n+    # (rtm-dst-len, dst); the default route carries no RTA_DST\n+    routes = {(0, None), (64, '2001:db8:6::'), (64, '2001:db8:5::')}\n+\n+    with NetNS() as ns_h, NetNS() as ns_r:\n+        ip(f\"link add veth0 netns {ns_h} type veth peer name veth1 netns {ns_r}\")\n+        with NetNSEnter(str(ns_h)):\n+            _write_ipv6_sysctl(\"conf/veth0/accept_ra\", 2)\n+            _write_ipv6_sysctl(\"conf/veth0/forwarding\", 0)\n+            try:\n+                _write_ipv6_sysctl(\"conf/veth0/accept_ra_rt_info_max_plen\", 64)\n+            except FileNotFoundError:\n+                raise KsftSkipEx(\"no CONFIG_IPV6_ROUTE_INFO\")\n+        with NetNSEnter(str(ns_r)):\n+            # skip the DAD probe so the router's link-local source only\n+            # has to wait for addrconf_dad_work() to clear IFA_F_TENTATIVE\n+            _write_ipv6_sysctl(\"conf/veth1/accept_dad\", 0)\n+        ip(\"link set dev veth0 up\", ns=str(ns_h))\n+        ip(\"link set dev veth1 up\", ns=str(ns_r))\n+\n+        rtnl = _rtnl_route_subscribe(ns_h)\n+        sock, ifindex = _ra_router_sock(ns_r, \"veth1\")\n+\n+        seen = _ra_advertise_routes(rtnl, sock, ifindex, routes,\n+                                    lifetime=1800,\n+                                    rio=('2001:db8:5::', 64, 600),\n+                                    pio=('2001:db8:6::', 64, 600))\n+        ksft_eq(set(seen), routes, \"not all RA routes were installed\")\n+        if set(seen) != routes:\n+            return\n+\n+        _send_ra(sock, ifindex, 0, rio=('2001:db8:5::', 64, 0),\n+                 pio=('2001:db8:6::', 64, 0))\n+        seen = _collect_route_ntfs(rtnl, 'delroute-ntf', routes)\n+        for key in routes:\n+            attrs = seen.get(key)\n+            ksft_not_none(attrs, f\"no RTM_DELROUTE for {key}\")\n+            if attrs is not None:\n+                ksft_eq(attrs.get('del-reason'), 'ra-withdrawn')\n+\n+\n+def ipv6_route_del_reason_absent() -\u003e None:\n+    \"\"\"\n+    A deletion path that records no cause (here a userspace request)\n+    must not carry RTA_DEL_REASON at all.\n+    \"\"\"\n+\n+    with NetNS() as ns:\n+        rtnl = _rtnl_route_subscribe(ns)\n+        ip(\"link add name dummy1 type dummy\", ns=str(ns))\n+        ip(\"link set dev dummy1 up\", ns=str(ns))\n+        ip(\"-6 route add 2001:db8:1::/64 dev dummy1\", ns=str(ns))\n+        ip(\"-6 route del 2001:db8:1::/64 dev dummy1\", ns=str(ns))\n+\n+        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:1::')\n+        ksft_not_none(attrs, \"no RTM_DELROUTE for 2001:db8:1::/64\")\n+        if attrs is not None:\n+            ksft_not_in('del-reason', attrs,\n+                        \"user deletion must not carry del-reason\")\n+\n+\n def main() -\u003e None:\n-    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify])\n+    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify,\n+              ipv6_route_del_reason_expired,\n+              ipv6_route_del_reason_ra_withdrawn,\n+              ipv6_route_del_reason_absent])\n     ksft_exit()\n \n if __name__ == \"__main__\":\n"
}

2/1 2026/08/08 01:20 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "IPV6",
    "IPV6_ROUTER_PREF",
    "IPV6_ROUTE_INFO"
  ],
  "FocusSymbols": [
    "ip6_del_rt_reason",
    "inet6_rt_del_notify",
    "fib6_del"
  ],
  "Reasoning": "The patch introduces a new netlink attribute RTA_DEL_REASON for IPv6 route deletion notifications, modifying core IPv6 routing and netlink message construction logic. This is a functional change that is reachable via route expiration, router advertisements, and manual route deletion, and should be fuzzed.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 7b445bd02f4f88497ebdad918798b8c83d196a91
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Aug 8 01:20:13 2026 +0000

    syz-cluster: applied patch under review

diff --git a/Documentation/netlink/specs/rt-route.yaml b/Documentation/netlink/specs/rt-route.yaml
index 33195db96746c..253037ea5176a 100644
--- a/Documentation/netlink/specs/rt-route.yaml
+++ b/Documentation/netlink/specs/rt-route.yaml
@@ -78,6 +78,27 @@ definitions:
       -
         name: rta-used
         type: u32
+  -
+    name: del-reason
+    type: enum
+    name-prefix: rt-del-reason-
+    enum-name: rt-del-reason
+    doc: |
+      Why the kernel deleted a route. New causes may be appended. The
+      value space is family-agnostic, a value is never reinterpreted
+      per address family.
+    entries:
+      -
+        name: unspec
+        doc: The deletion path does not record a cause.
+      -
+        name: expired
+        doc: The RTF_EXPIRES lifetime ran out and the route was garbage
+          collected.
+      -
+        name: ra-withdrawn
+        doc: A Router Advertisement withdrew the route with a zero
+          lifetime.
 
 attribute-sets:
   -
@@ -185,6 +206,18 @@ attribute-sets:
         type: u32
         byte-order: big-endian
         display-hint: hex
+      -
+        name: del-reason
+        type: u32
+        enum: del-reason
+        doc: |
+          Emitted only on RTM_DELROUTE notifications, and only when the
+          deletion path records a cause. An absent attribute means
+          either an older kernel or a deletion path that does not
+          record its cause, consumers must treat absent and unspec
+          identically. Currently only IPv6 deletion paths record a
+          cause. The attribute is notification-only, the kernel rejects
+          it in requests.
   -
     name: metrics
     name-prefix: rtax-
@@ -299,6 +332,7 @@ operations:
             - dport
             - nh-id
             - flowlabel
+            - del-reason
       dump:
         request:
           value: 26
@@ -313,7 +347,35 @@ operations:
       do:
         request:
           value: 24
-          attributes: *all-route-attrs
+          attributes: &route-req-attrs
+            - dst
+            - src
+            - iif
+            - oif
+            - gateway
+            - priority
+            - prefsrc
+            - metrics
+            - multipath
+            - flow
+            - cacheinfo
+            - table
+            - mark
+            - mfc-stats
+            - via
+            - newdst
+            - pref
+            - encap-type
+            - encap
+            - expires
+            - pad
+            - uid
+            - ttl-propagate
+            - ip-proto
+            - sport
+            - dport
+            - nh-id
+            - flowlabel
     -
       name: delroute
       doc: Delete an existing route
@@ -321,4 +383,23 @@ operations:
       do:
         request:
           value: 25
-          attributes: *all-route-attrs
+          attributes: *route-req-attrs
+    -
+      name: newroute-ntf
+      doc: Notification about a created route.
+      value: 24
+      notify: getroute
+    -
+      name: delroute-ntf
+      doc: Notification about a deleted route.
+      value: 25
+      notify: getroute
+
+mcast-groups:
+  list:
+    -
+      name: rtnlgrp-ipv4-route
+      value: 7
+    -
+      name: rtnlgrp-ipv6-route
+      value: 11
diff --git a/include/net/ip6_fib.h b/include/net/ip6_fib.h
index 9cd27e1b9b692..232289b439c3c 100644
--- a/include/net/ip6_fib.h
+++ b/include/net/ip6_fib.h
@@ -468,7 +468,8 @@ void fib6_clean_all_skip_notify(struct net *net,
 
 int fib6_add(struct fib6_node *root, struct fib6_info *rt,
 	     struct nl_info *info, struct netlink_ext_ack *extack);
-int fib6_del(struct fib6_info *rt, struct nl_info *info);
+int fib6_del(struct fib6_info *rt, struct nl_info *info,
+	     enum rt_del_reason del_reason);
 
 static inline
 void rt6_get_prefsrc(const struct rt6_info *rt, struct in6_addr *addr)
@@ -532,6 +533,8 @@ static inline void fib6_rt_update(struct net *net, struct fib6_info *rt,
 #endif
 void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
 		     unsigned int flags);
+void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info,
+			 enum rt_del_reason del_reason);
 
 void fib6_age_exceptions(struct fib6_info *rt, struct fib6_gc_args *gc_args,
 			 unsigned long now);
diff --git a/include/net/ip6_route.h b/include/net/ip6_route.h
index 09ffe0f13ce75..cc045705862de 100644
--- a/include/net/ip6_route.h
+++ b/include/net/ip6_route.h
@@ -126,6 +126,8 @@ int ipv6_route_ioctl(struct net *net, unsigned int cmd,
 int ip6_route_add(struct fib6_config *cfg, gfp_t gfp_flags,
 		  struct netlink_ext_ack *extack);
 int ip6_ins_rt(struct net *net, struct fib6_info *f6i);
+int ip6_del_rt_reason(struct net *net, struct fib6_info *f6i,
+		      enum rt_del_reason del_reason);
 #if IS_ENABLED(CONFIG_IPV6)
 int ip6_del_rt(struct net *net, struct fib6_info *f6i, bool skip_notify);
 #else
diff --git a/include/uapi/linux/rtnetlink.h b/include/uapi/linux/rtnetlink.h
index 27265fd31e5fc..3988b51db539f 100644
--- a/include/uapi/linux/rtnetlink.h
+++ b/include/uapi/linux/rtnetlink.h
@@ -399,6 +399,7 @@ enum rtattr_type_t {
 	RTA_DPORT,
 	RTA_NH_ID,
 	RTA_FLOWLABEL,
+	RTA_DEL_REASON,
 	__RTA_MAX
 };
 
@@ -407,6 +408,30 @@ enum rtattr_type_t {
 #define RTM_RTA(r)  ((struct rtattr*)(((char*)(r)) + NLMSG_ALIGN(sizeof(struct rtmsg))))
 #define RTM_PAYLOAD(n) NLMSG_PAYLOAD(n,sizeof(struct rtmsg))
 
+/* RTA_DEL_REASON: why the kernel deleted the route. u32.
+ * Emitted only on RTM_DELROUTE notifications, and only when the deletion
+ * path records a cause. Absence means either an older kernel or a
+ * deletion path that does not (yet) record its cause - consumers must
+ * treat "absent" and "unspec" identically. New causes may be appended.
+ * Currently only IPv6 deletion paths record a cause.
+ *
+ * The attribute is notification-only: the kernel rejects it in
+ * requests, so a notification must not be echoed back verbatim.
+ *
+ * The value space is family-agnostic: a value must never be
+ * reinterpreted per address family. A cause that only one family can
+ * produce still gets its own value rather than reusing another
+ * family's.
+ */
+enum rt_del_reason {
+	RT_DEL_REASON_UNSPEC,		/* cause not recorded */
+	RT_DEL_REASON_EXPIRED,		/* RTF_EXPIRES lifetime ran out (GC) */
+	RT_DEL_REASON_RA_WITHDRAWN,	/* zero-lifetime RA / PIO / RIO */
+	__RT_DEL_REASON_MAX
+};
+
+#define RT_DEL_REASON_MAX (__RT_DEL_REASON_MAX - 1)
+
 /* RTM_MULTIPATH --- array of struct rtnexthop.
  *
  * "struct rtnexthop" describes all necessary nexthop information,
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index f6fa2715b450b..9d89be7e0544b 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -2874,7 +2874,8 @@ void addrconf_prefix_rcv(struct net_device *dev, u8 *opt, int len, bool sllao)
 		if (rt) {
 			/* Autoconf prefix route */
 			if (valid_lft == 0) {
-				ip6_del_rt(net, rt, false);
+				ip6_del_rt_reason(net, rt,
+						  RT_DEL_REASON_RA_WITHDRAWN);
 				rt = NULL;
 			} else {
 				table = rt->fib6_table;
diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index e9fc692d4f3b2..3e382ba1573e3 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -1967,7 +1967,8 @@ static struct fib6_node *fib6_repair_tree(struct net *net,
 }
 
 static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn,
-			   struct fib6_info __rcu **rtp, struct nl_info *info)
+			   struct fib6_info __rcu **rtp, struct nl_info *info,
+			   enum rt_del_reason del_reason)
 {
 	struct fib6_info *leaf, *replace_rt = NULL;
 	struct fib6_walker *w;
@@ -2056,13 +2057,14 @@ static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn,
 			call_fib6_entry_notifiers_replace(net, replace_rt);
 	}
 	if (!info->skip_notify)
-		inet6_rt_notify(RTM_DELROUTE, rt, info, 0);
+		inet6_rt_del_notify(rt, info, del_reason);
 
 	fib6_info_release(rt);
 }
 
 /* Need to own table->tb6_lock */
-int fib6_del(struct fib6_info *rt, struct nl_info *info)
+int fib6_del(struct fib6_info *rt, struct nl_info *info,
+	     enum rt_del_reason del_reason)
 {
 	struct net *net = info->nl_net;
 	struct fib6_info __rcu **rtp;
@@ -2091,7 +2093,7 @@ int fib6_del(struct fib6_info *rt, struct nl_info *info)
 		if (rt == cur) {
 			if (fib6_requires_src(cur))
 				fib6_routes_require_src_dec(info->nl_net);
-			fib6_del_route(table, fn, rtp, info);
+			fib6_del_route(table, fn, rtp, info, del_reason);
 			return 0;
 		}
 		rtp_next = &cur->fib6_next;
@@ -2253,7 +2255,7 @@ static int fib6_clean_node(struct fib6_walker *w)
 		res = c->func(rt, c->arg);
 		if (res == -1) {
 			w->leaf = rt;
-			res = fib6_del(rt, &info);
+			res = fib6_del(rt, &info, RT_DEL_REASON_UNSPEC);
 			if (res) {
 #if RT6_DEBUG >= 2
 				pr_debug("%s: del failed: rt=%p@%p err=%d\n",
@@ -2401,7 +2403,7 @@ static void fib6_gc_table(struct net *net,
 
 	hlist_for_each_entry_safe(rt, n, &tb6->tb6_gc_hlist, gc_link)
 		if (fib6_age(rt, gc_args) == -1)
-			fib6_del(rt, &info);
+			fib6_del(rt, &info, RT_DEL_REASON_EXPIRED);
 }
 
 static void fib6_gc_all(struct net *net, struct fib6_gc_args *gc_args)
diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
index fe36b3f512850..1111c4ebc61d9 100644
--- a/net/ipv6/ndisc.c
+++ b/net/ipv6/ndisc.c
@@ -1361,7 +1361,9 @@ static enum skb_drop_reason ndisc_router_discovery(struct sk_buff *skb)
 	defrtr_usr_metric = in6_dev->cnf.ra_defrtr_metric;
 	/* delete the route if lifetime is 0 or if metric needs change */
 	if (rt && (lifetime == 0 || rt->fib6_metric != defrtr_usr_metric)) {
-		ip6_del_rt(net, rt, false);
+		ip6_del_rt_reason(net, rt,
+				  lifetime == 0 ? RT_DEL_REASON_RA_WITHDRAWN :
+						  RT_DEL_REASON_UNSPEC);
 		rt = NULL;
 	}
 
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index fc42d67e58226..1704359d3960d 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -111,7 +111,7 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 			 struct fib6_info *rt, struct dst_entry *dst,
 			 struct in6_addr *dest, struct in6_addr *src,
 			 int iif, int type, u32 portid, u32 seq,
-			 unsigned int flags);
+			 unsigned int flags, enum rt_del_reason del_reason);
 static struct rt6_info *rt6_find_cached_rt(const struct fib6_result *res,
 					   const struct in6_addr *daddr,
 					   const struct in6_addr *saddr);
@@ -1020,7 +1020,7 @@ int rt6_route_rcv(struct net_device *dev, u8 *opt, int len,
 					gwaddr, dev);
 
 	if (rt && !lifetime) {
-		ip6_del_rt(net, rt, false);
+		ip6_del_rt_reason(net, rt, RT_DEL_REASON_RA_WITHDRAWN);
 		rt = NULL;
 	}
 
@@ -3973,7 +3973,8 @@ int ip6_route_add(struct fib6_config *cfg, gfp_t gfp_flags,
 	return err;
 }
 
-static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info)
+static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info,
+			enum rt_del_reason del_reason)
 {
 	struct net *net = info->nl_net;
 	struct fib6_table *table;
@@ -3986,7 +3987,7 @@ static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info)
 
 	table = rt->fib6_table;
 	spin_lock_bh(&table->tb6_lock);
-	err = fib6_del(rt, info);
+	err = fib6_del(rt, info, del_reason);
 	spin_unlock_bh(&table->tb6_lock);
 
 out:
@@ -4001,7 +4002,15 @@ int ip6_del_rt(struct net *net, struct fib6_info *rt, bool skip_notify)
 		.skip_notify = skip_notify
 	};
 
-	return __ip6_del_rt(rt, &info);
+	return __ip6_del_rt(rt, &info, RT_DEL_REASON_UNSPEC);
+}
+
+int ip6_del_rt_reason(struct net *net, struct fib6_info *rt,
+		      enum rt_del_reason del_reason)
+{
+	struct nl_info info = { .nl_net = net };
+
+	return __ip6_del_rt(rt, &info, del_reason);
 }
 
 static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)
@@ -4028,7 +4037,8 @@ static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)
 
 			if (rt6_fill_node(net, skb, rt, NULL,
 					  NULL, NULL, 0, RTM_DELROUTE,
-					  info->portid, seq, 0) < 0) {
+					  info->portid, seq, 0,
+					  RT_DEL_REASON_UNSPEC) < 0) {
 				kfree_skb(skb);
 				skb = NULL;
 			} else
@@ -4064,13 +4074,13 @@ static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)
 		list_for_each_entry_safe(sibling, next_sibling,
 					 &rt->fib6_siblings,
 					 fib6_siblings) {
-			err = fib6_del(sibling, info);
+			err = fib6_del(sibling, info, RT_DEL_REASON_UNSPEC);
 			if (err)
 				goto out_unlock;
 		}
 	}
 
-	err = fib6_del(rt, info);
+	err = fib6_del(rt, info, RT_DEL_REASON_UNSPEC);
 out_unlock:
 	spin_unlock_bh(&table->tb6_lock);
 out_put:
@@ -4196,7 +4206,8 @@ static int ip6_route_del(struct fib6_config *cfg,
 				if (!fib6_info_hold_safe(rt))
 					continue;
 
-				err =  __ip6_del_rt(rt, &cfg->fc_nlinfo);
+				err = __ip6_del_rt(rt, &cfg->fc_nlinfo,
+						   RT_DEL_REASON_UNSPEC);
 				break;
 			}
 			if (cfg->fc_nh_id)
@@ -4215,7 +4226,8 @@ static int ip6_route_del(struct fib6_config *cfg,
 
 			/* if gateway was specified only delete the one hop */
 			if (cfg->fc_flags & RTF_GATEWAY)
-				err = __ip6_del_rt(rt, &cfg->fc_nlinfo);
+				err = __ip6_del_rt(rt, &cfg->fc_nlinfo,
+						   RT_DEL_REASON_UNSPEC);
 			else
 				err = __ip6_del_rt_siblings(rt, cfg);
 			break;
@@ -5739,6 +5751,7 @@ static size_t rt6_nlmsg_size(struct fib6_info *f6i)
 	       + nla_total_size(sizeof(struct rta_cacheinfo))
 	       + nla_total_size(TCP_CA_NAME_MAX) /* RTAX_CC_ALGO */
 	       + nla_total_size(1) /* RTA_PREF */
+	       + nla_total_size(4) /* RTA_DEL_REASON */
 	       + nexthop_len;
 }
 
@@ -5775,7 +5788,7 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 			 struct fib6_info *rt, struct dst_entry *dst,
 			 struct in6_addr *dest, struct in6_addr *src,
 			 int iif, int type, u32 portid, u32 seq,
-			 unsigned int flags)
+			 unsigned int flags, enum rt_del_reason del_reason)
 {
 	struct rt6_info *rt6 = dst_rt6_info(dst);
 	struct rt6key *rt6_dst, *rt6_src;
@@ -5954,6 +5967,10 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 	if (rtnl_put_cacheinfo(skb, dst, 0, expires, dst ? dst->error : 0) < 0)
 		goto nla_put_failure;
 
+	if (del_reason != RT_DEL_REASON_UNSPEC &&
+	    nla_put_u32(skb, RTA_DEL_REASON, del_reason))
+		goto nla_put_failure;
+
 	if (nla_put_u8(skb, RTA_PREF, IPV6_EXTRACT_PREF(rt6_flags)))
 		goto nla_put_failure;
 
@@ -6055,7 +6072,8 @@ static int rt6_nh_dump_exceptions(struct fib6_nh *nh, void *arg)
 					    &rt6_ex->rt6i->dst, NULL, NULL, 0,
 					    RTM_NEWROUTE,
 					    NETLINK_CB(dump->cb->skb).portid,
-					    dump->cb->nlh->nlmsg_seq, w->flags);
+					    dump->cb->nlh->nlmsg_seq, w->flags,
+					    RT_DEL_REASON_UNSPEC);
 			if (err)
 				return err;
 
@@ -6103,7 +6121,8 @@ int rt6_dump_route(struct fib6_info *rt, void *p_arg, unsigned int skip)
 			if (rt6_fill_node(net, arg->skb, rt, NULL, NULL, NULL,
 					  0, RTM_NEWROUTE,
 					  NETLINK_CB(arg->cb->skb).portid,
-					  arg->cb->nlh->nlmsg_seq, flags)) {
+					  arg->cb->nlh->nlmsg_seq, flags,
+					  RT_DEL_REASON_UNSPEC)) {
 				return 0;
 			}
 			count++;
@@ -6336,12 +6355,14 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,
 			err = rt6_fill_node(net, skb, from, NULL, NULL, NULL,
 					    iif, RTM_NEWROUTE,
 					    NETLINK_CB(in_skb).portid,
-					    nlh->nlmsg_seq, 0);
+					    nlh->nlmsg_seq, 0,
+					    RT_DEL_REASON_UNSPEC);
 		else
 			err = rt6_fill_node(net, skb, from, dst, &fl6.daddr,
 					    &fl6.saddr, iif, RTM_NEWROUTE,
 					    NETLINK_CB(in_skb).portid,
-					    nlh->nlmsg_seq, 0);
+					    nlh->nlmsg_seq, 0,
+					    RT_DEL_REASON_UNSPEC);
 	} else {
 		err = -ENETUNREACH;
 	}
@@ -6357,8 +6378,9 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,
 	return err;
 }
 
-void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
-		     unsigned int nlm_flags)
+static void __inet6_rt_notify(int event, struct fib6_info *rt,
+			      struct nl_info *info, unsigned int nlm_flags,
+			      enum rt_del_reason del_reason)
 {
 	struct net *net = info->nl_net;
 	struct sk_buff *skb;
@@ -6377,7 +6399,7 @@ void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
 		goto errout;
 
 	err = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,
-			    event, info->portid, seq, nlm_flags);
+			    event, info->portid, seq, nlm_flags, del_reason);
 	if (err < 0) {
 		kfree_skb(skb);
 		/* -EMSGSIZE implies needed space grew under us. */
@@ -6398,6 +6420,18 @@ void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
 	rtnl_set_sk_err(net, RTNLGRP_IPV6_ROUTE, err);
 }
 
+void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
+		     unsigned int nlm_flags)
+{
+	__inet6_rt_notify(event, rt, info, nlm_flags, RT_DEL_REASON_UNSPEC);
+}
+
+void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info,
+			 enum rt_del_reason del_reason)
+{
+	__inet6_rt_notify(RTM_DELROUTE, rt, info, 0, del_reason);
+}
+
 void fib6_rt_update(struct net *net, struct fib6_info *rt,
 		    struct nl_info *info)
 {
@@ -6410,7 +6444,8 @@ void fib6_rt_update(struct net *net, struct fib6_info *rt,
 		goto errout;
 
 	err = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,
-			    RTM_NEWROUTE, info->portid, seq, NLM_F_REPLACE);
+			    RTM_NEWROUTE, info->portid, seq, NLM_F_REPLACE,
+			    RT_DEL_REASON_UNSPEC);
 	if (err < 0) {
 		/* -EMSGSIZE implies BUG in rt6_nlmsg_size() */
 		WARN_ON(err == -EMSGSIZE);
@@ -6463,7 +6498,7 @@ void fib6_info_hw_flags_set(struct net *net, struct fib6_info *f6i,
 	}
 
 	err = rt6_fill_node(net, skb, f6i, NULL, NULL, NULL, 0, RTM_NEWROUTE, 0,
-			    0, 0);
+			    0, 0, RT_DEL_REASON_UNSPEC);
 	if (err < 0) {
 		/* -EMSGSIZE implies BUG in rt6_nlmsg_size() */
 		WARN_ON(err == -EMSGSIZE);
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index e58bdbdc58eea..34935886b6adf 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py
@@ -17,7 +17,7 @@ from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
     wait_file, tool, tc
 from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
 from .ynl import NlError, NlctrlFamily, YnlFamily, \
-    EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily
+    EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily
 from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink
 
 __all__ = ["KSRC",
@@ -34,4 +34,4 @@ __all__ = ["KSRC",
            "NetdevSim", "NetdevSimDev",
            "NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError",
            "YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily",
-           "NlctrlFamily", "RtnlAddrFamily", "Netlink"]
+           "NlctrlFamily", "RtnlAddrFamily", "RtnlRouteFamily", "Netlink"]
diff --git a/tools/testing/selftests/net/lib/py/ynl.py b/tools/testing/selftests/net/lib/py/ynl.py
index 2e567062aa6cc..08deff756f29b 100644
--- a/tools/testing/selftests/net/lib/py/ynl.py
+++ b/tools/testing/selftests/net/lib/py/ynl.py
@@ -29,7 +29,7 @@ except ModuleNotFoundError as e:
 
 __all__ = [
     "NlError", "NlPolicy", "Netlink", "YnlFamily", "SPEC_PATH",
-    "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily",
+    "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily", "RtnlRouteFamily",
     "NetdevFamily", "NetshaperFamily", "NlctrlFamily", "DevlinkFamily",
     "PSPFamily",
 ]
@@ -54,6 +54,11 @@ class RtnlAddrFamily(YnlFamily):
         super().__init__((SPEC_PATH / Path('rt-addr.yaml')).as_posix(),
                          schema='', recv_size=recv_size)
 
+class RtnlRouteFamily(YnlFamily):
+    def __init__(self, recv_size=0):
+        super().__init__((SPEC_PATH / Path('rt-route.yaml')).as_posix(),
+                         schema='', recv_size=recv_size)
+
 class NetdevFamily(YnlFamily):
     def __init__(self, recv_size=0):
         super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(),
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index 0c67c7c00d841..5cc3ebdcf08de 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py
@@ -5,7 +5,9 @@ import socket
 import struct
 import time
 from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx
-from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily
+from lib.py import ksft_not_in, ksft_not_none
+from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily, RtnlRouteFamily
+from lib.py import defer
 
 IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
 IPV4_TEST_MULTICAST = b'\xef\x01\x01\x01'
@@ -134,8 +136,189 @@ def ipv4_devconf_notify() -> None:
     ksft_true(f"inet {ifname} forwarding on" in cmd_obj.stdout,
               f"No 'forwarding on' notificiation found for interface {ifname}")
 
+def _rtnl_route_subscribe(ns):
+    with NetNSEnter(str(ns)):
+        rtnl = RtnlRouteFamily()
+    defer(rtnl.close)
+    rtnl.ntf_subscribe("rtnlgrp-ipv6-route")
+    return rtnl
+
+
+def _wait_route_ntf(rtnl, name, dst_len, dst=None, deadline=10):
+    """Return the attrs of the first matching notification, None on timeout."""
+
+    for msg in rtnl.poll_ntf(duration=deadline):
+        if msg['name'] != name:
+            continue
+        attrs = msg['msg']
+        if attrs['rtm-dst-len'] != dst_len:
+            continue
+        if dst is not None and attrs.get('dst') != dst:
+            continue
+        return attrs
+    return None
+
+
+def _collect_route_ntfs(rtnl, name, want, deadline=10):
+    """Gather attrs of matching notifications, keyed by (dst_len, dst)."""
+
+    seen = {}
+    for msg in rtnl.poll_ntf(duration=deadline):
+        if msg['name'] != name:
+            continue
+        attrs = msg['msg']
+        key = (attrs['rtm-dst-len'], attrs.get('dst'))
+        if key in want:
+            seen[key] = attrs
+            if len(seen) == len(want):
+                break
+    return seen
+
+
+def _write_ipv6_sysctl(name, value):
+    with open(f"/proc/sys/net/ipv6/{name}", "w") as f:
+        f.write(f"{value}\n")
+
+
+def ipv6_route_del_reason_expired() -> None:
+    """An expired route reports RTA_DEL_REASON == expired."""
+
+    with NetNS() as ns:
+        rtnl = _rtnl_route_subscribe(ns)
+        with NetNSEnter(str(ns)):
+            _write_ipv6_sysctl("route/gc_interval", 2)
+        ip("link add name dummy1 type dummy", ns=str(ns))
+        ip("link set dev dummy1 up", ns=str(ns))
+        ip("-6 route add 2001:db8:2::/64 dev dummy1 expires 2", ns=str(ns))
+
+        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:2::',
+                                deadline=15)
+        ksft_not_none(attrs, "no RTM_DELROUTE for the expired route")
+        if attrs is not None:
+            ksft_eq(attrs.get('del-reason'), 'expired')
+
+
+def _send_ra(sock, ifindex, lifetime, rio=None, pio=None):
+    """The kernel fills in the ICMPv6 checksum on raw ICMPv6 sockets."""
+
+    # type, code, cksum, hop limit, flags, router lifetime,
+    # reachable time, retrans timer
+    ra = struct.pack('!BBHBBHII', 134, 0, 0, 64, 0, lifetime, 0, 0)
+    if rio is not None:
+        prefix, plen, rio_lifetime = rio
+        # RFC 4191 route information option, /64 prefix (8 bytes)
+        ra += struct.pack('!BBBBI', 24, 2, plen, 0, rio_lifetime)
+        ra += socket.inet_pton(socket.AF_INET6, prefix)[:8]
+    if pio is not None:
+        prefix, plen, valid_lft = pio
+        # RFC 4861 prefix information option, on-link only (L set, A clear)
+        ra += struct.pack('!BBBBIII', 3, 4, plen, 0x80, valid_lft, 0, 0)
+        ra += socket.inet_pton(socket.AF_INET6, prefix)
+    sock.sendto(ra, ('ff02::1', 0, 0, ifindex))
+
+
+def _ra_router_sock(ns_r, ifname):
+    with NetNSEnter(str(ns_r)):
+        sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW,
+                             socket.IPPROTO_ICMPV6)
+        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
+        defer(sock.close)
+        return sock, socket.if_nametoindex(ifname)
+
+
+def _ra_advertise_routes(rtnl, sock, ifindex, want, **ra_opts):
+    """
+    Sending fails with EADDRNOTAVAIL while the router's link-local
+    address is still tentative. addrconf_dad_start() only queues
+    addrconf_dad_work(), and IFA_F_TENTATIVE is cleared when that work
+    item runs, so retry until it does.
+    """
+
+    seen = {}
+    for _ in range(10):
+        try:
+            _send_ra(sock, ifindex, **ra_opts)
+        except OSError:
+            time.sleep(0.2)
+            continue
+        seen.update(_collect_route_ntfs(rtnl, 'newroute-ntf',
+                                        want - set(seen.keys()), deadline=2))
+        if len(seen) == len(want):
+            break
+    return seen
+
+
+def ipv6_route_del_reason_ra_withdrawn() -> None:
+    """
+    Routes withdrawn by a zero-lifetime RA (router lifetime, RFC 4861
+    PIO, RFC 4191 RIO) report RTA_DEL_REASON == ra-withdrawn.
+    """
+
+    # (rtm-dst-len, dst); the default route carries no RTA_DST
+    routes = {(0, None), (64, '2001:db8:6::'), (64, '2001:db8:5::')}
+
+    with NetNS() as ns_h, NetNS() as ns_r:
+        ip(f"link add veth0 netns {ns_h} type veth peer name veth1 netns {ns_r}")
+        with NetNSEnter(str(ns_h)):
+            _write_ipv6_sysctl("conf/veth0/accept_ra", 2)
+            _write_ipv6_sysctl("conf/veth0/forwarding", 0)
+            try:
+                _write_ipv6_sysctl("conf/veth0/accept_ra_rt_info_max_plen", 64)
+            except FileNotFoundError:
+                raise KsftSkipEx("no CONFIG_IPV6_ROUTE_INFO")
+        with NetNSEnter(str(ns_r)):
+            # skip the DAD probe so the router's link-local source only
+            # has to wait for addrconf_dad_work() to clear IFA_F_TENTATIVE
+            _write_ipv6_sysctl("conf/veth1/accept_dad", 0)
+        ip("link set dev veth0 up", ns=str(ns_h))
+        ip("link set dev veth1 up", ns=str(ns_r))
+
+        rtnl = _rtnl_route_subscribe(ns_h)
+        sock, ifindex = _ra_router_sock(ns_r, "veth1")
+
+        seen = _ra_advertise_routes(rtnl, sock, ifindex, routes,
+                                    lifetime=1800,
+                                    rio=('2001:db8:5::', 64, 600),
+                                    pio=('2001:db8:6::', 64, 600))
+        ksft_eq(set(seen), routes, "not all RA routes were installed")
+        if set(seen) != routes:
+            return
+
+        _send_ra(sock, ifindex, 0, rio=('2001:db8:5::', 64, 0),
+                 pio=('2001:db8:6::', 64, 0))
+        seen = _collect_route_ntfs(rtnl, 'delroute-ntf', routes)
+        for key in routes:
+            attrs = seen.get(key)
+            ksft_not_none(attrs, f"no RTM_DELROUTE for {key}")
+            if attrs is not None:
+                ksft_eq(attrs.get('del-reason'), 'ra-withdrawn')
+
+
+def ipv6_route_del_reason_absent() -> None:
+    """
+    A deletion path that records no cause (here a userspace request)
+    must not carry RTA_DEL_REASON at all.
+    """
+
+    with NetNS() as ns:
+        rtnl = _rtnl_route_subscribe(ns)
+        ip("link add name dummy1 type dummy", ns=str(ns))
+        ip("link set dev dummy1 up", ns=str(ns))
+        ip("-6 route add 2001:db8:1::/64 dev dummy1", ns=str(ns))
+        ip("-6 route del 2001:db8:1::/64 dev dummy1", ns=str(ns))
+
+        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:1::')
+        ksft_not_none(attrs, "no RTM_DELROUTE for 2001:db8:1::/64")
+        if attrs is not None:
+            ksft_not_in('del-reason', attrs,
+                        "user deletion must not carry del-reason")
+
+
 def main() -> None:
-    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify])
+    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify,
+              ipv6_route_del_reason_expired,
+              ipv6_route_del_reason_ra_withdrawn,
+              ipv6_route_del_reason_absent])
     ksft_exit()
 
 if __name__ == "__main__":

3/2 2026/08/08 01:20 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 13057
							output: 35
							thoughts: 713
						

Thoughts:
**Fuzzing Candidate: IPv6 Route Deletion Reason**

Okay, so I've got this patch to review, and the task is to figure out if it introduces functional changes in the kernel that warrant fuzzing. Right off the bat, I see it's touching a bunch of IPv6 routing code – `Documentation/netlink/specs/rt-route.yaml`, various header files (`ip6_fib.h`, `ip6_route.h`, `rtnetlink.h`), core IPv6 network files (`addrconf.c`, `ip6_fib.c`, `ndisc.c`, `route.c`), and even some python tests. That's a strong signal, this is probably going to be interesting.

The core of the patch revolves around adding a new Netlink attribute, `RTA_DEL_REASON`, specifically for IPv6 route deletion notifications. They're adding an `enum rt_del_reason` with values to specify why the route is being deleted: `UNSPEC`, `EXPIRED`, or `RA_WITHDRAWN`. That alone tells me this *is* a functional change, a significant one at that. It alters how we communicate about route deletions through Netlink, which is core to the control plane.

So, the question is, is this reachable, and should we fuzz it? Absolutely. We're talking about IPv6 route deletions – that's a *very* common operation. RA withdrawal, route expiration, manual deletion – all of these paths now need to take this new deletion reason into account. The patch itself modifies functions like `fib6_del`, `ip6_del_rt_reason`, `rt6_fill_node`, `inet6_rt_notify`, implying that a good portion of the IPv6 stack is being impacted.

Now, let's get down to brass tacks: which functions should we prioritize for fuzzing? Looking at the specific functions modified, I need to focus on these:

*   `addrconf_prefix_rcv`
*   `fib6_del`
*   `fib6_del_route`
*   `fib6_clean_node`
*   `fib6_gc_table`
*   `ndisc_router_discovery`
*   `rt6_route_rcv`
*   `ip6_del_rt_reason`
*   `__ip6_del_rt`
*   `ip6_del_rt`
*   `__ip6_del_rt_siblings`
*   `ip6_route_del`
*   `rt6_nlmsg_size`
*   `rt6_fill_node`
*   `rt6_nh_dump_exceptions`
*   `rt6_dump_route`
*   `inet6_rtm_getroute`
*   `__inet6_rt_notify`
*   `inet6_rt_notify`
*   `inet6_rt_del_notify`
*   `fib6_rt_update`
*   `fib6_info_hw_flags_set`

Narrowing this further, I need to zoom in on the specific symbols: `ip6_del_rt_reason`, `inet6_rt_del_notify`, `rt6_fill_node`, and `rt6_nlmsg_size`. They are clearly critical for handling and constructing the new Netlink messages.

Finally, I need to check for any configuration dependencies. I already know that `CONFIG_IPV6` is required, which is a given, but there’s a reference to `CONFIG_IPV6_ROUTE_INFO` in the tests. I'll need to double-check `net/ipv6/Kconfig` to see what that actually controls and if it's necessary for this patch to work. I want to make sure the fuzzing environment is set up correctly.

In short, this is definitely a good candidate for fuzzing. We have a new Netlink attribute being added, and that means new code paths, and new logic for message construction, and IPv6 routing operations – all of which needs to be rigorously tested.



4/2 2026/08/08 01:20 tool grepper
0m Args:
{
  "Expression": "config IPV6_ROUTE_INFO",
  "PathPrefix": "net/ipv6/Kconfig"
}

Results:
{
  "Output": "net/ipv6/Kconfig=21=config IPV6_ROUTER_PREF\n--\nnet/ipv6/Kconfig-30-\nnet/ipv6/Kconfig:31:config IPV6_ROUTE_INFO\nnet/ipv6/Kconfig-32-\tbool \"IPv6: Route Information (RFC 4191) support\"\n"
}

5/2 2026/08/08 01:20 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 14802
							output: 34
							thoughts: 17
						

Thoughts:
Here's your summary:

**Digging Deeper into IPv6 Configuration**

Okay, I need to make sure I'm not missing anything crucial, especially with IPv6. I know the basics, but I need to be thorough. Let's not just assume I've covered all the bases. I should really check the configuration for `IPV6_ROUTER_PREF`. That setting's important for understanding how the router prioritizes IPv6 routes, and it could be the key to understanding any connectivity issues or unexpected routing behavior I'm seeing. It's too easy to overlook the finer points when you're focusing on the general setup. This could explain a lot. This setting is frequently missed, and should always be considered.



6/2 2026/08/08 01:20 tool read-file
0m Args:
{
  "File": "net/ipv6/Kconfig",
  "FirstLine": 21,
  "LineCount": 20
}

Results:
{
  "Contents": "  21:\tconfig IPV6_ROUTER_PREF\n  22:\t\tbool \"IPv6: Router Preference (RFC 4191) support\"\n  23:\t\thelp\n  24:\t\t  Router Preference is an optional extension to the Router\n  25:\t\t  Advertisement message which improves the ability of hosts\n  26:\t\t  to pick an appropriate router, especially when the hosts\n  27:\t\t  are placed in a multi-homed network.\n  28:\t\n  29:\t\t  If unsure, say N.\n  30:\t\n  31:\tconfig IPV6_ROUTE_INFO\n  32:\t\tbool \"IPv6: Route Information (RFC 4191) support\"\n  33:\t\tdepends on IPV6_ROUTER_PREF\n  34:\t\thelp\n  35:\t\t  Support of Route Information.\n  36:\t\n  37:\t\t  If unsure, say N.\n  38:\t\n  39:\tconfig IPV6_OPTIMISTIC_DAD\n  40:\t\tbool \"IPv6: Enable RFC 4429 Optimistic DAD\"\n"
}

7/2 2026/08/08 01:20 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 15294
							output: 34
							thoughts: 166
						

Thoughts:
**My Initial Assessment of the IPv6 Route Configuration Patch**

Okay, so looking at this code, it's clear that the changes revolve around IPv6 routing, specifically concerning `IPV6_ROUTE_INFO` which appears to be linked to `IPV6_ROUTER_PREF`. And of course, the use of `IPV6` is fundamental to all of this.

I'm getting the sense there are likely other configurations involved here, so I'll keep an eye out for those.

The patch seems to be touching core IPv6 components: `net/ipv6/addrconf.c`, `net/ipv6/ip6_fib.c`, `net/ipv6/ndisc.c`, and `net/ipv6/route.c`. So we are dealing with pretty fundamental functionality.

Now, I need to focus on the key symbols that will tell the core story: specifically, I'll pay close attention to `ip6_del_rt_reason`, `inet6_rt_del_notify`, `rt6_fill_node`, and `rt6_nlmsg_size`. They should provide context for what's going on.

Finally, I need to make sure that `rt6_fill_node` is not static.



8/2 2026/08/08 01:20 tool grepper
0m Args:
{
  "Expression": "rt6_fill_node",
  "PathPrefix": "net/ipv6/route.c"
}

Results:
{
  "Output": "net/ipv6/route.c=109=static size_t rt6_nlmsg_size(struct fib6_info *f6i);\nnet/ipv6/route.c:110:static int rt6_fill_node(struct net *net, struct sk_buff *skb,\nnet/ipv6/route.c-111-\t\t\t struct fib6_info *rt, struct dst_entry *dst,\n--\nnet/ipv6/route.c=4016=static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)\n--\nnet/ipv6/route.c-4037-\nnet/ipv6/route.c:4038:\t\t\tif (rt6_fill_node(net, skb, rt, NULL,\nnet/ipv6/route.c-4039-\t\t\t\t\t  NULL, NULL, 0, RTM_DELROUTE,\n--\nnet/ipv6/route.c=5711=static size_t rt6_nlmsg_size(struct fib6_info *f6i)\n--\nnet/ipv6/route.c-5757-\nnet/ipv6/route.c:5758:static int rt6_fill_node_nexthop(struct sk_buff *skb, struct nexthop *nh,\nnet/ipv6/route.c-5759-\t\t\t\t unsigned char *flags)\n--\nnet/ipv6/route.c-5786-\nnet/ipv6/route.c:5787:static int rt6_fill_node(struct net *net, struct sk_buff *skb,\nnet/ipv6/route.c-5788-\t\t\t struct fib6_info *rt, struct dst_entry *dst,\n--\nnet/ipv6/route.c-5940-\t\tif (READ_ONCE(net-\u003eipv4.sysctl_nexthop_compat_mode) \u0026\u0026\nnet/ipv6/route.c:5941:\t\t    rt6_fill_node_nexthop(skb, rt-\u003enh, \u0026nh_flags) \u003c 0)\nnet/ipv6/route.c-5942-\t\t\tgoto nla_put_failure;\n--\nnet/ipv6/route.c=6036=static int rt6_nh_dump_exceptions(struct fib6_nh *nh, void *arg)\n--\nnet/ipv6/route.c-6070-\nnet/ipv6/route.c:6071:\t\t\terr = rt6_fill_node(dump-\u003enet, dump-\u003eskb, w-\u003ert,\nnet/ipv6/route.c-6072-\t\t\t\t\t    \u0026rt6_ex-\u003ert6i-\u003edst, NULL, NULL, 0,\n--\nnet/ipv6/route.c=6089=int rt6_dump_route(struct fib6_info *rt, void *p_arg, unsigned int skip)\n--\nnet/ipv6/route.c-6120-\t\t} else {\nnet/ipv6/route.c:6121:\t\t\tif (rt6_fill_node(net, arg-\u003eskb, rt, NULL, NULL, NULL,\nnet/ipv6/route.c-6122-\t\t\t\t\t  0, RTM_NEWROUTE,\n--\nnet/ipv6/route.c=6232=static int inet6_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,\n--\nnet/ipv6/route.c-6354-\t\tif (fibmatch)\nnet/ipv6/route.c:6355:\t\t\terr = rt6_fill_node(net, skb, from, NULL, NULL, NULL,\nnet/ipv6/route.c-6356-\t\t\t\t\t    iif, RTM_NEWROUTE,\n--\nnet/ipv6/route.c-6360-\t\telse\nnet/ipv6/route.c:6361:\t\t\terr = rt6_fill_node(net, skb, from, dst, \u0026fl6.daddr,\nnet/ipv6/route.c-6362-\t\t\t\t\t    \u0026fl6.saddr, iif, RTM_NEWROUTE,\n--\nnet/ipv6/route.c=6381=static void __inet6_rt_notify(int event, struct fib6_info *rt,\n--\nnet/ipv6/route.c-6400-\nnet/ipv6/route.c:6401:\terr = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,\nnet/ipv6/route.c-6402-\t\t\t    event, info-\u003eportid, seq, nlm_flags, del_reason);\n--\nnet/ipv6/route.c=6435=void fib6_rt_update(struct net *net, struct fib6_info *rt,\n--\nnet/ipv6/route.c-6445-\nnet/ipv6/route.c:6446:\terr = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,\nnet/ipv6/route.c-6447-\t\t\t    RTM_NEWROUTE, info-\u003eportid, seq, NLM_F_REPLACE,\n--\nnet/ipv6/route.c=6462=void fib6_info_hw_flags_set(struct net *net, struct fib6_info *f6i,\n--\nnet/ipv6/route.c-6499-\nnet/ipv6/route.c:6500:\terr = rt6_fill_node(net, skb, f6i, NULL, NULL, NULL, 0, RTM_NEWROUTE, 0,\nnet/ipv6/route.c-6501-\t\t\t    0, 0, RT_DEL_REASON_UNSPEC);\n"
}

9/2 2026/08/08 01:20 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 17216
							output: 36
							thoughts: 86
						

Thoughts:
Here's my summary, as I see it:

**Route Table Maintenance - Static vs. Dynamic Functions**

Okay, so I'm looking at these route table functions and trying to figure out the scope and access levels. `rt6_fill_node` is defined as static, meaning it's only visible within the current compilation unit. That makes sense for internal helper functions, not something intended for external use. However, `ip6_del_rt_reason` and `inet6_rt_del_notify` are defined as *exported*, or non-static, functions. That means they're intended to be callable from other parts of the kernel. Finally, `rt6_nlmsg_size` is static, which is probably used to size the netlink message for the route.

Given this initial observation about the difference in scoping, I need to check out these exported functions, `ip6_del_rt_reason` and `inet6_rt_del_notify`, a bit more closely to fully understand their purpose within the overall routing implementation. I need to understand how other kernel components might be leveraging these particular functions.



10/2 2026/08/08 01:20 tool grepper
0m Args:
{
  "Expression": "ip6_del_rt_reason",
  "PathPrefix": "net/ipv6/route.c"
}

Results:
{
  "Output": "net/ipv6/route.c=970=int rt6_route_rcv(struct net_device *dev, u8 *opt, int len,\n--\nnet/ipv6/route.c-1022-\tif (rt \u0026\u0026 !lifetime) {\nnet/ipv6/route.c:1023:\t\tip6_del_rt_reason(net, rt, RT_DEL_REASON_RA_WITHDRAWN);\nnet/ipv6/route.c-1024-\t\trt = NULL;\n--\nnet/ipv6/route.c=3998=int ip6_del_rt(struct net *net, struct fib6_info *rt, bool skip_notify)\n--\nnet/ipv6/route.c-4007-\nnet/ipv6/route.c:4008:int ip6_del_rt_reason(struct net *net, struct fib6_info *rt,\nnet/ipv6/route.c-4009-\t\t      enum rt_del_reason del_reason)\n"
}

11/2 2026/08/08 01:20 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 17840
							output: 36
							thoughts: 13
						

Thoughts:
Here's my attempt at summarizing that thought process, tailored for an expert audience:

**Considering IPv6 Routing Table Updates**

Hmm, so we're talking about routing, and specifically the implications of a route deletion notification. My immediate thought is, what's the behavior of `inet6_rt_del_notify` in this scenario? I need to quickly recall the exact context of that function. Does it directly trigger updates to the forwarding information base (FIB), or does it signal some other component? I need to consider its role within the broader IPv6 stack, including interactions with neighbor discovery, prefix delegation, and route caching. I also need to think about potential race conditions and locking within the routing table update process. Are we considering a scenario involving dynamic routing protocols? The implications might differ significantly depending on whether this is a static route deletion, or something more complex like a BGP route withdrawal. Let's delve deeper into this aspect.



12/2 2026/08/08 01:20 tool grepper
0m Args:
{
  "Expression": "inet6_rt_del_notify",
  "PathPrefix": "net/ipv6/route.c"
}

Results:
{
  "Output": "net/ipv6/route.c=6423=void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,\n--\nnet/ipv6/route.c-6428-\nnet/ipv6/route.c:6429:void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info,\nnet/ipv6/route.c-6430-\t\t\t enum rt_del_reason del_reason)\n"
}

13/2 2026/08/08 01:20 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 18221
							output: 35
							thoughts: 19
						

Thoughts:
Okay, here's my take on those thoughts, framed as an expert's internal monologue:

**Thinking Through Function Mutability**

Alright, let's see, I'm trying to wrap my head around potential function side effects, particularly focusing on how dynamic they really are. The code appears to use `fib6_del` which has been modified. My immediate question is, beyond this, am I missing any other places where a non-static function's internal state might be altered? I have to be exhaustive here to fully understand the implications of function state changes in the broader system. I need to know where there could be hidden mutable state. If I miss even one point, this oversight would undermine my entire analysis.



14/2 2026/08/08 01:20 tool grepper
0m Args:
{
  "Expression": "fib6_del",
  "PathPrefix": "net/ipv6/ip6_fib.c"
}

Results:
{
  "Output": "net/ipv6/ip6_fib.c=1841=static struct fib6_node *fib6_repair_tree(struct net *net,\n--\nnet/ipv6/ip6_fib.c-1968-\nnet/ipv6/ip6_fib.c:1969:static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn,\nnet/ipv6/ip6_fib.c-1970-\t\t\t   struct fib6_info __rcu **rtp, struct nl_info *info,\n--\nnet/ipv6/ip6_fib.c-2065-/* Need to own table-\u003etb6_lock */\nnet/ipv6/ip6_fib.c:2066:int fib6_del(struct fib6_info *rt, struct nl_info *info,\nnet/ipv6/ip6_fib.c-2067-\t     enum rt_del_reason del_reason)\n--\nnet/ipv6/ip6_fib.c-2095-\t\t\t\tfib6_routes_require_src_dec(info-\u003enl_net);\nnet/ipv6/ip6_fib.c:2096:\t\t\tfib6_del_route(table, fn, rtp, info, del_reason);\nnet/ipv6/ip6_fib.c-2097-\t\t\treturn 0;\n--\nnet/ipv6/ip6_fib.c-2107- *\tCertainly, it is not interrupt safe.\nnet/ipv6/ip6_fib.c:2108: *\tHowever, it is internally reenterable wrt itself and fib6_add/fib6_del.\nnet/ipv6/ip6_fib.c-2109- *\tIt means, that we can modify tree during walking\n--\nnet/ipv6/ip6_fib.c=2234=static int fib6_clean_node(struct fib6_walker *w)\n--\nnet/ipv6/ip6_fib.c-2257-\t\t\tw-\u003eleaf = rt;\nnet/ipv6/ip6_fib.c:2258:\t\t\tres = fib6_del(rt, \u0026info, RT_DEL_REASON_UNSPEC);\nnet/ipv6/ip6_fib.c-2259-\t\t\tif (res) {\n--\nnet/ipv6/ip6_fib.c=2393=static void fib6_gc_table(struct net *net,\n--\nnet/ipv6/ip6_fib.c-2405-\t\tif (fib6_age(rt, gc_args) == -1)\nnet/ipv6/ip6_fib.c:2406:\t\t\tfib6_del(rt, \u0026info, RT_DEL_REASON_EXPIRED);\nnet/ipv6/ip6_fib.c-2407-}\n"
}

15/2 2026/08/08 01:20 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 19137
							output: 144
							thoughts: 397
						

Thoughts:
**My Analysis of IPv6 Route Deletion**

Okay, let's break this down. My initial observation is that `fib6_del` isn't declared `static`, which is important context.

My core focus here is on understanding the relevant symbols and configurations related to IPv6 route deletion. My key symbols are: `ip6_del_rt_reason`, `inet6_rt_del_notify`, and `fib6_del`.

Now, regarding configurations, the essential ones are clearly `IPV6`, but I need to consider others. `IPV6_ROUTER_PREF` and `IPV6_ROUTE_INFO` are specific for testing Route Information Options (RIOs) in the Python test suite, specifically for the `RT_DEL_REASON_RA_WITHDRAWN` reason. However, the foundational `RTA_DEL_REASON` functionality actually works without those flags - for things like expired routes or regular Router Advertisements. It’s definitely beneficial to have those configs enabled for thorough testing of the RIO aspects though. So they are important for completeness.

I need to confirm if any other configs are relevant. `CONFIG_NET` is a given, but what about `CONFIG_IPV6_MULTIPLE_TABLES`? I don't think that's strictly required for this specific functionality.

Next, I'll quickly check the signatures of these key functions to ensure they align with my understanding.

*   `fib6_del`: `int fib6_del(struct fib6_info *rt, struct nl_info *info, enum rt_del_reason del_reason)` - Looks good.
*   `ip6_del_rt_reason`: `int ip6_del_rt_reason(struct net *net, struct fib6_info *rt, enum rt_del_reason del_reason)` - Also good.
*   `inet6_rt_del_notify`: `void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info, enum rt_del_reason del_reason)` - Correct.

Alright, these focus symbols are all aligned with the expected interfaces. I'm now ready to move forward.



16/2 2026/08/08 01:20 tool set-results
0m Args:
{
  "EnableConfigs": [
    "IPV6",
    "IPV6_ROUTER_PREF",
    "IPV6_ROUTE_INFO"
  ],
  "FocusSymbols": [
    "ip6_del_rt_reason",
    "inet6_rt_del_notify",
    "fib6_del"
  ],
  "Reasoning": "The patch introduces a new netlink attribute RTA_DEL_REASON for IPv6 route deletion notifications, modifying core IPv6 routing and netlink message construction logic. This is a functional change that is reachable via route expiration, router advertisements, and manual route deletion, and should be fuzzed.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "IPV6",
    "IPV6_ROUTER_PREF",
    "IPV6_ROUTE_INFO"
  ],
  "FocusSymbols": [
    "ip6_del_rt_reason",
    "inet6_rt_del_notify",
    "fib6_del"
  ],
  "Reasoning": "The patch introduces a new netlink attribute RTA_DEL_REASON for IPv6 route deletion notifications, modifying core IPv6 routing and netlink message construction logic. This is a functional change that is reachable via route expiration, router advertisements, and manual route deletion, and should be fuzzed.",
  "WorthFuzzing": true
}

17/1 2026/08/08 01:20 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

18/2 2026/08/08 01:20 agent kmsan-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "KMSANReasoning": "The patch introduces a new netlink attribute `RTA_DEL_REASON` to report the reason for IPv6 route deletions. The deletion reason is passed through the routing subsystem as an explicit `enum rt_del_reason` value (e.g., `RT_DEL_REASON_UNSPEC`, `RT_DEL_REASON_EXPIRED`, `RT_DEL_REASON_RA_WITHDRAWN`). This value is then serialized into the netlink message using `nla_put_u32()`. \n\nSince the `del_reason` is passed by value as an explicit constant in all modified call sites, there is no risk of reading uninitialized memory from stack, heap, or padding bytes. The netlink attribute construction via `nla_put_u32()` safely handles the serialization without exposing uninitialized kernel memory to user space. Therefore, standard KASAN and other bug detectors are sufficient to catch any potential memory safety issues (like out-of-bounds accesses or use-after-free) in this code, and a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 7b445bd02f4f88497ebdad918798b8c83d196a91
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Aug 8 01:20:13 2026 +0000

    syz-cluster: applied patch under review

diff --git a/Documentation/netlink/specs/rt-route.yaml b/Documentation/netlink/specs/rt-route.yaml
index 33195db96746c..253037ea5176a 100644
--- a/Documentation/netlink/specs/rt-route.yaml
+++ b/Documentation/netlink/specs/rt-route.yaml
@@ -78,6 +78,27 @@ definitions:
       -
         name: rta-used
         type: u32
+  -
+    name: del-reason
+    type: enum
+    name-prefix: rt-del-reason-
+    enum-name: rt-del-reason
+    doc: |
+      Why the kernel deleted a route. New causes may be appended. The
+      value space is family-agnostic, a value is never reinterpreted
+      per address family.
+    entries:
+      -
+        name: unspec
+        doc: The deletion path does not record a cause.
+      -
+        name: expired
+        doc: The RTF_EXPIRES lifetime ran out and the route was garbage
+          collected.
+      -
+        name: ra-withdrawn
+        doc: A Router Advertisement withdrew the route with a zero
+          lifetime.
 
 attribute-sets:
   -
@@ -185,6 +206,18 @@ attribute-sets:
         type: u32
         byte-order: big-endian
         display-hint: hex
+      -
+        name: del-reason
+        type: u32
+        enum: del-reason
+        doc: |
+          Emitted only on RTM_DELROUTE notifications, and only when the
+          deletion path records a cause. An absent attribute means
+          either an older kernel or a deletion path that does not
+          record its cause, consumers must treat absent and unspec
+          identically. Currently only IPv6 deletion paths record a
+          cause. The attribute is notification-only, the kernel rejects
+          it in requests.
   -
     name: metrics
     name-prefix: rtax-
@@ -299,6 +332,7 @@ operations:
             - dport
             - nh-id
             - flowlabel
+            - del-reason
       dump:
         request:
           value: 26
@@ -313,7 +347,35 @@ operations:
       do:
         request:
           value: 24
-          attributes: *all-route-attrs
+          attributes: &route-req-attrs
+            - dst
+            - src
+            - iif
+            - oif
+            - gateway
+            - priority
+            - prefsrc
+            - metrics
+            - multipath
+            - flow
+            - cacheinfo
+            - table
+            - mark
+            - mfc-stats
+            - via
+            - newdst
+            - pref
+            - encap-type
+            - encap
+            - expires
+            - pad
+            - uid
+            - ttl-propagate
+            - ip-proto
+            - sport
+            - dport
+            - nh-id
+            - flowlabel
     -
       name: delroute
       doc: Delete an existing route
@@ -321,4 +383,23 @@ operations:
       do:
         request:
           value: 25
-          attributes: *all-route-attrs
+          attributes: *route-req-attrs
+    -
+      name: newroute-ntf
+      doc: Notification about a created route.
+      value: 24
+      notify: getroute
+    -
+      name: delroute-ntf
+      doc: Notification about a deleted route.
+      value: 25
+      notify: getroute
+
+mcast-groups:
+  list:
+    -
+      name: rtnlgrp-ipv4-route
+      value: 7
+    -
+      name: rtnlgrp-ipv6-route
+      value: 11
diff --git a/include/net/ip6_fib.h b/include/net/ip6_fib.h
index 9cd27e1b9b692..232289b439c3c 100644
--- a/include/net/ip6_fib.h
+++ b/include/net/ip6_fib.h
@@ -468,7 +468,8 @@ void fib6_clean_all_skip_notify(struct net *net,
 
 int fib6_add(struct fib6_node *root, struct fib6_info *rt,
 	     struct nl_info *info, struct netlink_ext_ack *extack);
-int fib6_del(struct fib6_info *rt, struct nl_info *info);
+int fib6_del(struct fib6_info *rt, struct nl_info *info,
+	     enum rt_del_reason del_reason);
 
 static inline
 void rt6_get_prefsrc(const struct rt6_info *rt, struct in6_addr *addr)
@@ -532,6 +533,8 @@ static inline void fib6_rt_update(struct net *net, struct fib6_info *rt,
 #endif
 void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
 		     unsigned int flags);
+void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info,
+			 enum rt_del_reason del_reason);
 
 void fib6_age_exceptions(struct fib6_info *rt, struct fib6_gc_args *gc_args,
 			 unsigned long now);
diff --git a/include/net/ip6_route.h b/include/net/ip6_route.h
index 09ffe0f13ce75..cc045705862de 100644
--- a/include/net/ip6_route.h
+++ b/include/net/ip6_route.h
@@ -126,6 +126,8 @@ int ipv6_route_ioctl(struct net *net, unsigned int cmd,
 int ip6_route_add(struct fib6_config *cfg, gfp_t gfp_flags,
 		  struct netlink_ext_ack *extack);
 int ip6_ins_rt(struct net *net, struct fib6_info *f6i);
+int ip6_del_rt_reason(struct net *net, struct fib6_info *f6i,
+		      enum rt_del_reason del_reason);
 #if IS_ENABLED(CONFIG_IPV6)
 int ip6_del_rt(struct net *net, struct fib6_info *f6i, bool skip_notify);
 #else
diff --git a/include/uapi/linux/rtnetlink.h b/include/uapi/linux/rtnetlink.h
index 27265fd31e5fc..3988b51db539f 100644
--- a/include/uapi/linux/rtnetlink.h
+++ b/include/uapi/linux/rtnetlink.h
@@ -399,6 +399,7 @@ enum rtattr_type_t {
 	RTA_DPORT,
 	RTA_NH_ID,
 	RTA_FLOWLABEL,
+	RTA_DEL_REASON,
 	__RTA_MAX
 };
 
@@ -407,6 +408,30 @@ enum rtattr_type_t {
 #define RTM_RTA(r)  ((struct rtattr*)(((char*)(r)) + NLMSG_ALIGN(sizeof(struct rtmsg))))
 #define RTM_PAYLOAD(n) NLMSG_PAYLOAD(n,sizeof(struct rtmsg))
 
+/* RTA_DEL_REASON: why the kernel deleted the route. u32.
+ * Emitted only on RTM_DELROUTE notifications, and only when the deletion
+ * path records a cause. Absence means either an older kernel or a
+ * deletion path that does not (yet) record its cause - consumers must
+ * treat "absent" and "unspec" identically. New causes may be appended.
+ * Currently only IPv6 deletion paths record a cause.
+ *
+ * The attribute is notification-only: the kernel rejects it in
+ * requests, so a notification must not be echoed back verbatim.
+ *
+ * The value space is family-agnostic: a value must never be
+ * reinterpreted per address family. A cause that only one family can
+ * produce still gets its own value rather than reusing another
+ * family's.
+ */
+enum rt_del_reason {
+	RT_DEL_REASON_UNSPEC,		/* cause not recorded */
+	RT_DEL_REASON_EXPIRED,		/* RTF_EXPIRES lifetime ran out (GC) */
+	RT_DEL_REASON_RA_WITHDRAWN,	/* zero-lifetime RA / PIO / RIO */
+	__RT_DEL_REASON_MAX
+};
+
+#define RT_DEL_REASON_MAX (__RT_DEL_REASON_MAX - 1)
+
 /* RTM_MULTIPATH --- array of struct rtnexthop.
  *
  * "struct rtnexthop" describes all necessary nexthop information,
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index f6fa2715b450b..9d89be7e0544b 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -2874,7 +2874,8 @@ void addrconf_prefix_rcv(struct net_device *dev, u8 *opt, int len, bool sllao)
 		if (rt) {
 			/* Autoconf prefix route */
 			if (valid_lft == 0) {
-				ip6_del_rt(net, rt, false);
+				ip6_del_rt_reason(net, rt,
+						  RT_DEL_REASON_RA_WITHDRAWN);
 				rt = NULL;
 			} else {
 				table = rt->fib6_table;
diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index e9fc692d4f3b2..3e382ba1573e3 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -1967,7 +1967,8 @@ static struct fib6_node *fib6_repair_tree(struct net *net,
 }
 
 static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn,
-			   struct fib6_info __rcu **rtp, struct nl_info *info)
+			   struct fib6_info __rcu **rtp, struct nl_info *info,
+			   enum rt_del_reason del_reason)
 {
 	struct fib6_info *leaf, *replace_rt = NULL;
 	struct fib6_walker *w;
@@ -2056,13 +2057,14 @@ static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn,
 			call_fib6_entry_notifiers_replace(net, replace_rt);
 	}
 	if (!info->skip_notify)
-		inet6_rt_notify(RTM_DELROUTE, rt, info, 0);
+		inet6_rt_del_notify(rt, info, del_reason);
 
 	fib6_info_release(rt);
 }
 
 /* Need to own table->tb6_lock */
-int fib6_del(struct fib6_info *rt, struct nl_info *info)
+int fib6_del(struct fib6_info *rt, struct nl_info *info,
+	     enum rt_del_reason del_reason)
 {
 	struct net *net = info->nl_net;
 	struct fib6_info __rcu **rtp;
@@ -2091,7 +2093,7 @@ int fib6_del(struct fib6_info *rt, struct nl_info *info)
 		if (rt == cur) {
 			if (fib6_requires_src(cur))
 				fib6_routes_require_src_dec(info->nl_net);
-			fib6_del_route(table, fn, rtp, info);
+			fib6_del_route(table, fn, rtp, info, del_reason);
 			return 0;
 		}
 		rtp_next = &cur->fib6_next;
@@ -2253,7 +2255,7 @@ static int fib6_clean_node(struct fib6_walker *w)
 		res = c->func(rt, c->arg);
 		if (res == -1) {
 			w->leaf = rt;
-			res = fib6_del(rt, &info);
+			res = fib6_del(rt, &info, RT_DEL_REASON_UNSPEC);
 			if (res) {
 #if RT6_DEBUG >= 2
 				pr_debug("%s: del failed: rt=%p@%p err=%d\n",
@@ -2401,7 +2403,7 @@ static void fib6_gc_table(struct net *net,
 
 	hlist_for_each_entry_safe(rt, n, &tb6->tb6_gc_hlist, gc_link)
 		if (fib6_age(rt, gc_args) == -1)
-			fib6_del(rt, &info);
+			fib6_del(rt, &info, RT_DEL_REASON_EXPIRED);
 }
 
 static void fib6_gc_all(struct net *net, struct fib6_gc_args *gc_args)
diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
index fe36b3f512850..1111c4ebc61d9 100644
--- a/net/ipv6/ndisc.c
+++ b/net/ipv6/ndisc.c
@@ -1361,7 +1361,9 @@ static enum skb_drop_reason ndisc_router_discovery(struct sk_buff *skb)
 	defrtr_usr_metric = in6_dev->cnf.ra_defrtr_metric;
 	/* delete the route if lifetime is 0 or if metric needs change */
 	if (rt && (lifetime == 0 || rt->fib6_metric != defrtr_usr_metric)) {
-		ip6_del_rt(net, rt, false);
+		ip6_del_rt_reason(net, rt,
+				  lifetime == 0 ? RT_DEL_REASON_RA_WITHDRAWN :
+						  RT_DEL_REASON_UNSPEC);
 		rt = NULL;
 	}
 
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index fc42d67e58226..1704359d3960d 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -111,7 +111,7 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 			 struct fib6_info *rt, struct dst_entry *dst,
 			 struct in6_addr *dest, struct in6_addr *src,
 			 int iif, int type, u32 portid, u32 seq,
-			 unsigned int flags);
+			 unsigned int flags, enum rt_del_reason del_reason);
 static struct rt6_info *rt6_find_cached_rt(const struct fib6_result *res,
 					   const struct in6_addr *daddr,
 					   const struct in6_addr *saddr);
@@ -1020,7 +1020,7 @@ int rt6_route_rcv(struct net_device *dev, u8 *opt, int len,
 					gwaddr, dev);
 
 	if (rt && !lifetime) {
-		ip6_del_rt(net, rt, false);
+		ip6_del_rt_reason(net, rt, RT_DEL_REASON_RA_WITHDRAWN);
 		rt = NULL;
 	}
 
@@ -3973,7 +3973,8 @@ int ip6_route_add(struct fib6_config *cfg, gfp_t gfp_flags,
 	return err;
 }
 
-static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info)
+static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info,
+			enum rt_del_reason del_reason)
 {
 	struct net *net = info->nl_net;
 	struct fib6_table *table;
@@ -3986,7 +3987,7 @@ static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info)
 
 	table = rt->fib6_table;
 	spin_lock_bh(&table->tb6_lock);
-	err = fib6_del(rt, info);
+	err = fib6_del(rt, info, del_reason);
 	spin_unlock_bh(&table->tb6_lock);
 
 out:
@@ -4001,7 +4002,15 @@ int ip6_del_rt(struct net *net, struct fib6_info *rt, bool skip_notify)
 		.skip_notify = skip_notify
 	};
 
-	return __ip6_del_rt(rt, &info);
+	return __ip6_del_rt(rt, &info, RT_DEL_REASON_UNSPEC);
+}
+
+int ip6_del_rt_reason(struct net *net, struct fib6_info *rt,
+		      enum rt_del_reason del_reason)
+{
+	struct nl_info info = { .nl_net = net };
+
+	return __ip6_del_rt(rt, &info, del_reason);
 }
 
 static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)
@@ -4028,7 +4037,8 @@ static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)
 
 			if (rt6_fill_node(net, skb, rt, NULL,
 					  NULL, NULL, 0, RTM_DELROUTE,
-					  info->portid, seq, 0) < 0) {
+					  info->portid, seq, 0,
+					  RT_DEL_REASON_UNSPEC) < 0) {
 				kfree_skb(skb);
 				skb = NULL;
 			} else
@@ -4064,13 +4074,13 @@ static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config *cfg)
 		list_for_each_entry_safe(sibling, next_sibling,
 					 &rt->fib6_siblings,
 					 fib6_siblings) {
-			err = fib6_del(sibling, info);
+			err = fib6_del(sibling, info, RT_DEL_REASON_UNSPEC);
 			if (err)
 				goto out_unlock;
 		}
 	}
 
-	err = fib6_del(rt, info);
+	err = fib6_del(rt, info, RT_DEL_REASON_UNSPEC);
 out_unlock:
 	spin_unlock_bh(&table->tb6_lock);
 out_put:
@@ -4196,7 +4206,8 @@ static int ip6_route_del(struct fib6_config *cfg,
 				if (!fib6_info_hold_safe(rt))
 					continue;
 
-				err =  __ip6_del_rt(rt, &cfg->fc_nlinfo);
+				err = __ip6_del_rt(rt, &cfg->fc_nlinfo,
+						   RT_DEL_REASON_UNSPEC);
 				break;
 			}
 			if (cfg->fc_nh_id)
@@ -4215,7 +4226,8 @@ static int ip6_route_del(struct fib6_config *cfg,
 
 			/* if gateway was specified only delete the one hop */
 			if (cfg->fc_flags & RTF_GATEWAY)
-				err = __ip6_del_rt(rt, &cfg->fc_nlinfo);
+				err = __ip6_del_rt(rt, &cfg->fc_nlinfo,
+						   RT_DEL_REASON_UNSPEC);
 			else
 				err = __ip6_del_rt_siblings(rt, cfg);
 			break;
@@ -5739,6 +5751,7 @@ static size_t rt6_nlmsg_size(struct fib6_info *f6i)
 	       + nla_total_size(sizeof(struct rta_cacheinfo))
 	       + nla_total_size(TCP_CA_NAME_MAX) /* RTAX_CC_ALGO */
 	       + nla_total_size(1) /* RTA_PREF */
+	       + nla_total_size(4) /* RTA_DEL_REASON */
 	       + nexthop_len;
 }
 
@@ -5775,7 +5788,7 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 			 struct fib6_info *rt, struct dst_entry *dst,
 			 struct in6_addr *dest, struct in6_addr *src,
 			 int iif, int type, u32 portid, u32 seq,
-			 unsigned int flags)
+			 unsigned int flags, enum rt_del_reason del_reason)
 {
 	struct rt6_info *rt6 = dst_rt6_info(dst);
 	struct rt6key *rt6_dst, *rt6_src;
@@ -5954,6 +5967,10 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 	if (rtnl_put_cacheinfo(skb, dst, 0, expires, dst ? dst->error : 0) < 0)
 		goto nla_put_failure;
 
+	if (del_reason != RT_DEL_REASON_UNSPEC &&
+	    nla_put_u32(skb, RTA_DEL_REASON, del_reason))
+		goto nla_put_failure;
+
 	if (nla_put_u8(skb, RTA_PREF, IPV6_EXTRACT_PREF(rt6_flags)))
 		goto nla_put_failure;
 
@@ -6055,7 +6072,8 @@ static int rt6_nh_dump_exceptions(struct fib6_nh *nh, void *arg)
 					    &rt6_ex->rt6i->dst, NULL, NULL, 0,
 					    RTM_NEWROUTE,
 					    NETLINK_CB(dump->cb->skb).portid,
-					    dump->cb->nlh->nlmsg_seq, w->flags);
+					    dump->cb->nlh->nlmsg_seq, w->flags,
+					    RT_DEL_REASON_UNSPEC);
 			if (err)
 				return err;
 
@@ -6103,7 +6121,8 @@ int rt6_dump_route(struct fib6_info *rt, void *p_arg, unsigned int skip)
 			if (rt6_fill_node(net, arg->skb, rt, NULL, NULL, NULL,
 					  0, RTM_NEWROUTE,
 					  NETLINK_CB(arg->cb->skb).portid,
-					  arg->cb->nlh->nlmsg_seq, flags)) {
+					  arg->cb->nlh->nlmsg_seq, flags,
+					  RT_DEL_REASON_UNSPEC)) {
 				return 0;
 			}
 			count++;
@@ -6336,12 +6355,14 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,
 			err = rt6_fill_node(net, skb, from, NULL, NULL, NULL,
 					    iif, RTM_NEWROUTE,
 					    NETLINK_CB(in_skb).portid,
-					    nlh->nlmsg_seq, 0);
+					    nlh->nlmsg_seq, 0,
+					    RT_DEL_REASON_UNSPEC);
 		else
 			err = rt6_fill_node(net, skb, from, dst, &fl6.daddr,
 					    &fl6.saddr, iif, RTM_NEWROUTE,
 					    NETLINK_CB(in_skb).portid,
-					    nlh->nlmsg_seq, 0);
+					    nlh->nlmsg_seq, 0,
+					    RT_DEL_REASON_UNSPEC);
 	} else {
 		err = -ENETUNREACH;
 	}
@@ -6357,8 +6378,9 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,
 	return err;
 }
 
-void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
-		     unsigned int nlm_flags)
+static void __inet6_rt_notify(int event, struct fib6_info *rt,
+			      struct nl_info *info, unsigned int nlm_flags,
+			      enum rt_del_reason del_reason)
 {
 	struct net *net = info->nl_net;
 	struct sk_buff *skb;
@@ -6377,7 +6399,7 @@ void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
 		goto errout;
 
 	err = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,
-			    event, info->portid, seq, nlm_flags);
+			    event, info->portid, seq, nlm_flags, del_reason);
 	if (err < 0) {
 		kfree_skb(skb);
 		/* -EMSGSIZE implies needed space grew under us. */
@@ -6398,6 +6420,18 @@ void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
 	rtnl_set_sk_err(net, RTNLGRP_IPV6_ROUTE, err);
 }
 
+void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info,
+		     unsigned int nlm_flags)
+{
+	__inet6_rt_notify(event, rt, info, nlm_flags, RT_DEL_REASON_UNSPEC);
+}
+
+void inet6_rt_del_notify(struct fib6_info *rt, struct nl_info *info,
+			 enum rt_del_reason del_reason)
+{
+	__inet6_rt_notify(RTM_DELROUTE, rt, info, 0, del_reason);
+}
+
 void fib6_rt_update(struct net *net, struct fib6_info *rt,
 		    struct nl_info *info)
 {
@@ -6410,7 +6444,8 @@ void fib6_rt_update(struct net *net, struct fib6_info *rt,
 		goto errout;
 
 	err = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0,
-			    RTM_NEWROUTE, info->portid, seq, NLM_F_REPLACE);
+			    RTM_NEWROUTE, info->portid, seq, NLM_F_REPLACE,
+			    RT_DEL_REASON_UNSPEC);
 	if (err < 0) {
 		/* -EMSGSIZE implies BUG in rt6_nlmsg_size() */
 		WARN_ON(err == -EMSGSIZE);
@@ -6463,7 +6498,7 @@ void fib6_info_hw_flags_set(struct net *net, struct fib6_info *f6i,
 	}
 
 	err = rt6_fill_node(net, skb, f6i, NULL, NULL, NULL, 0, RTM_NEWROUTE, 0,
-			    0, 0);
+			    0, 0, RT_DEL_REASON_UNSPEC);
 	if (err < 0) {
 		/* -EMSGSIZE implies BUG in rt6_nlmsg_size() */
 		WARN_ON(err == -EMSGSIZE);
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index e58bdbdc58eea..34935886b6adf 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py
@@ -17,7 +17,7 @@ from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
     wait_file, tool, tc
 from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
 from .ynl import NlError, NlctrlFamily, YnlFamily, \
-    EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily
+    EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily
 from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink
 
 __all__ = ["KSRC",
@@ -34,4 +34,4 @@ __all__ = ["KSRC",
            "NetdevSim", "NetdevSimDev",
            "NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError",
            "YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily",
-           "NlctrlFamily", "RtnlAddrFamily", "Netlink"]
+           "NlctrlFamily", "RtnlAddrFamily", "RtnlRouteFamily", "Netlink"]
diff --git a/tools/testing/selftests/net/lib/py/ynl.py b/tools/testing/selftests/net/lib/py/ynl.py
index 2e567062aa6cc..08deff756f29b 100644
--- a/tools/testing/selftests/net/lib/py/ynl.py
+++ b/tools/testing/selftests/net/lib/py/ynl.py
@@ -29,7 +29,7 @@ except ModuleNotFoundError as e:
 
 __all__ = [
     "NlError", "NlPolicy", "Netlink", "YnlFamily", "SPEC_PATH",
-    "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily",
+    "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily", "RtnlRouteFamily",
     "NetdevFamily", "NetshaperFamily", "NlctrlFamily", "DevlinkFamily",
     "PSPFamily",
 ]
@@ -54,6 +54,11 @@ class RtnlAddrFamily(YnlFamily):
         super().__init__((SPEC_PATH / Path('rt-addr.yaml')).as_posix(),
                          schema='', recv_size=recv_size)
 
+class RtnlRouteFamily(YnlFamily):
+    def __init__(self, recv_size=0):
+        super().__init__((SPEC_PATH / Path('rt-route.yaml')).as_posix(),
+                         schema='', recv_size=recv_size)
+
 class NetdevFamily(YnlFamily):
     def __init__(self, recv_size=0):
         super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(),
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index 0c67c7c00d841..5cc3ebdcf08de 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py
@@ -5,7 +5,9 @@ import socket
 import struct
 import time
 from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx
-from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily
+from lib.py import ksft_not_in, ksft_not_none
+from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily, RtnlRouteFamily
+from lib.py import defer
 
 IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
 IPV4_TEST_MULTICAST = b'\xef\x01\x01\x01'
@@ -134,8 +136,189 @@ def ipv4_devconf_notify() -> None:
     ksft_true(f"inet {ifname} forwarding on" in cmd_obj.stdout,
               f"No 'forwarding on' notificiation found for interface {ifname}")
 
+def _rtnl_route_subscribe(ns):
+    with NetNSEnter(str(ns)):
+        rtnl = RtnlRouteFamily()
+    defer(rtnl.close)
+    rtnl.ntf_subscribe("rtnlgrp-ipv6-route")
+    return rtnl
+
+
+def _wait_route_ntf(rtnl, name, dst_len, dst=None, deadline=10):
+    """Return the attrs of the first matching notification, None on timeout."""
+
+    for msg in rtnl.poll_ntf(duration=deadline):
+        if msg['name'] != name:
+            continue
+        attrs = msg['msg']
+        if attrs['rtm-dst-len'] != dst_len:
+            continue
+        if dst is not None and attrs.get('dst') != dst:
+            continue
+        return attrs
+    return None
+
+
+def _collect_route_ntfs(rtnl, name, want, deadline=10):
+    """Gather attrs of matching notifications, keyed by (dst_len, dst)."""
+
+    seen = {}
+    for msg in rtnl.poll_ntf(duration=deadline):
+        if msg['name'] != name:
+            continue
+        attrs = msg['msg']
+        key = (attrs['rtm-dst-len'], attrs.get('dst'))
+        if key in want:
+            seen[key] = attrs
+            if len(seen) == len(want):
+                break
+    return seen
+
+
+def _write_ipv6_sysctl(name, value):
+    with open(f"/proc/sys/net/ipv6/{name}", "w") as f:
+        f.write(f"{value}\n")
+
+
+def ipv6_route_del_reason_expired() -> None:
+    """An expired route reports RTA_DEL_REASON == expired."""
+
+    with NetNS() as ns:
+        rtnl = _rtnl_route_subscribe(ns)
+        with NetNSEnter(str(ns)):
+            _write_ipv6_sysctl("route/gc_interval", 2)
+        ip("link add name dummy1 type dummy", ns=str(ns))
+        ip("link set dev dummy1 up", ns=str(ns))
+        ip("-6 route add 2001:db8:2::/64 dev dummy1 expires 2", ns=str(ns))
+
+        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:2::',
+                                deadline=15)
+        ksft_not_none(attrs, "no RTM_DELROUTE for the expired route")
+        if attrs is not None:
+            ksft_eq(attrs.get('del-reason'), 'expired')
+
+
+def _send_ra(sock, ifindex, lifetime, rio=None, pio=None):
+    """The kernel fills in the ICMPv6 checksum on raw ICMPv6 sockets."""
+
+    # type, code, cksum, hop limit, flags, router lifetime,
+    # reachable time, retrans timer
+    ra = struct.pack('!BBHBBHII', 134, 0, 0, 64, 0, lifetime, 0, 0)
+    if rio is not None:
+        prefix, plen, rio_lifetime = rio
+        # RFC 4191 route information option, /64 prefix (8 bytes)
+        ra += struct.pack('!BBBBI', 24, 2, plen, 0, rio_lifetime)
+        ra += socket.inet_pton(socket.AF_INET6, prefix)[:8]
+    if pio is not None:
+        prefix, plen, valid_lft = pio
+        # RFC 4861 prefix information option, on-link only (L set, A clear)
+        ra += struct.pack('!BBBBIII', 3, 4, plen, 0x80, valid_lft, 0, 0)
+        ra += socket.inet_pton(socket.AF_INET6, prefix)
+    sock.sendto(ra, ('ff02::1', 0, 0, ifindex))
+
+
+def _ra_router_sock(ns_r, ifname):
+    with NetNSEnter(str(ns_r)):
+        sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW,
+                             socket.IPPROTO_ICMPV6)
+        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
+        defer(sock.close)
+        return sock, socket.if_nametoindex(ifname)
+
+
+def _ra_advertise_routes(rtnl, sock, ifindex, want, **ra_opts):
+    """
+    Sending fails with EADDRNOTAVAIL while the router's link-local
+    address is still tentative. addrconf_dad_start() only queues
+    addrconf_dad_work(), and IFA_F_TENTATIVE is cleared when that work
+    item runs, so retry until it does.
+    """
+
+    seen = {}
+    for _ in range(10):
+        try:
+            _send_ra(sock, ifindex, **ra_opts)
+        except OSError:
+            time.sleep(0.2)
+            continue
+        seen.update(_collect_route_ntfs(rtnl, 'newroute-ntf',
+                                        want - set(seen.keys()), deadline=2))
+        if len(seen) == len(want):
+            break
+    return seen
+
+
+def ipv6_route_del_reason_ra_withdrawn() -> None:
+    """
+    Routes withdrawn by a zero-lifetime RA (router lifetime, RFC 4861
+    PIO, RFC 4191 RIO) report RTA_DEL_REASON == ra-withdrawn.
+    """
+
+    # (rtm-dst-len, dst); the default route carries no RTA_DST
+    routes = {(0, None), (64, '2001:db8:6::'), (64, '2001:db8:5::')}
+
+    with NetNS() as ns_h, NetNS() as ns_r:
+        ip(f"link add veth0 netns {ns_h} type veth peer name veth1 netns {ns_r}")
+        with NetNSEnter(str(ns_h)):
+            _write_ipv6_sysctl("conf/veth0/accept_ra", 2)
+            _write_ipv6_sysctl("conf/veth0/forwarding", 0)
+            try:
+                _write_ipv6_sysctl("conf/veth0/accept_ra_rt_info_max_plen", 64)
+            except FileNotFoundError:
+                raise KsftSkipEx("no CONFIG_IPV6_ROUTE_INFO")
+        with NetNSEnter(str(ns_r)):
+            # skip the DAD probe so the router's link-local source only
+            # has to wait for addrconf_dad_work() to clear IFA_F_TENTATIVE
+            _write_ipv6_sysctl("conf/veth1/accept_dad", 0)
+        ip("link set dev veth0 up", ns=str(ns_h))
+        ip("link set dev veth1 up", ns=str(ns_r))
+
+        rtnl = _rtnl_route_subscribe(ns_h)
+        sock, ifindex = _ra_router_sock(ns_r, "veth1")
+
+        seen = _ra_advertise_routes(rtnl, sock, ifindex, routes,
+                                    lifetime=1800,
+                                    rio=('2001:db8:5::', 64, 600),
+                                    pio=('2001:db8:6::', 64, 600))
+        ksft_eq(set(seen), routes, "not all RA routes were installed")
+        if set(seen) != routes:
+            return
+
+        _send_ra(sock, ifindex, 0, rio=('2001:db8:5::', 64, 0),
+                 pio=('2001:db8:6::', 64, 0))
+        seen = _collect_route_ntfs(rtnl, 'delroute-ntf', routes)
+        for key in routes:
+            attrs = seen.get(key)
+            ksft_not_none(attrs, f"no RTM_DELROUTE for {key}")
+            if attrs is not None:
+                ksft_eq(attrs.get('del-reason'), 'ra-withdrawn')
+
+
+def ipv6_route_del_reason_absent() -> None:
+    """
+    A deletion path that records no cause (here a userspace request)
+    must not carry RTA_DEL_REASON at all.
+    """
+
+    with NetNS() as ns:
+        rtnl = _rtnl_route_subscribe(ns)
+        ip("link add name dummy1 type dummy", ns=str(ns))
+        ip("link set dev dummy1 up", ns=str(ns))
+        ip("-6 route add 2001:db8:1::/64 dev dummy1", ns=str(ns))
+        ip("-6 route del 2001:db8:1::/64 dev dummy1", ns=str(ns))
+
+        attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:1::')
+        ksft_not_none(attrs, "no RTM_DELROUTE for 2001:db8:1::/64")
+        if attrs is not None:
+            ksft_not_in('del-reason', attrs,
+                        "user deletion must not carry del-reason")
+
+
 def main() -> None:
-    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify])
+    ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify,
+              ipv6_route_del_reason_expired,
+              ipv6_route_del_reason_ra_withdrawn,
+              ipv6_route_del_reason_absent])
     ksft_exit()
 
 if __name__ == "__main__":

19/3 2026/08/08 01:20 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 13054
							output: 258
							thoughts: 1984
						

Thoughts:
**KMSAN Evaluation: IPv6 Route Deletion Reason**

Okay, let's break this down. We're looking at a patch series that introduces a `del_reason` netlink attribute for IPv6 route deletion notifications. The question is: does this warrant a dedicated KMSAN fuzzing session?

My initial thought is to scrutinize the code for any potential uninitialized memory issues that KMSAN could catch. The patch's core introduces an `RTA_DEL_REASON` attribute and an associated `enum rt_del_reason`. This `del_reason` is then plumbed through various functions in the IPv6 routing code. Specifically, the code passes `del_reason` via function arguments (e.g., in `rt6_fill_node`, `fib6_del`, `ip6_del_rt_reason`).

The critical point here is that `del_reason` is *passed by value* and is always an *explicitly assigned enum value* (like `RT_DEL_REASON_UNSPEC`, `RT_DEL_REASON_EXPIRED`, or `RT_DEL_REASON_RA_WITHDRAWN`). It is *not* read from uninitialized struct fields. We are also not introducing any new structs that would be sent to user space. It is directly placed into the netlink buffer using `nla_put_u32`.

I'm checking if we can get a sneakily uninitialized variable to user space, but there isn't one. The netlink attribute is constructed using `nla_put_u32`, which handles the memory and is completely safe. The `del_reason` parameter is passed by value and is set to a constant.

I'm also double-checking the callers to ensure all function calls use explicit enum values. I've scanned the various locations in the code (e.g., `__ip6_del_rt_siblings`, `fib6_del`, etc.) and confirmed that constant enum values are being passed everywhere. No uninitialized memory is involved.

Given the explicit nature of the values and the use of standard, safe netlink functions, I conclude that there is *no* risk of uninitialized memory being exposed to user space. Standard KASAN and other bug detectors should be sufficient to identify any issues. Therefore, no dedicated KMSAN session is needed for this patch. I can confidently set `NeedsKMSAN` to `false`.



20/3 2026/08/08 01:21 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch introduces a new netlink attribute `RTA_DEL_REASON` to report the reason for IPv6 route deletions. The deletion reason is passed through the routing subsystem as an explicit `enum rt_del_reason` value (e.g., `RT_DEL_REASON_UNSPEC`, `RT_DEL_REASON_EXPIRED`, `RT_DEL_REASON_RA_WITHDRAWN`). This value is then serialized into the netlink message using `nla_put_u32()`. \n\nSince the `del_reason` is passed by value as an explicit constant in all modified call sites, there is no risk of reading uninitialized memory from stack, heap, or padding bytes. The netlink attribute construction via `nla_put_u32()` safely handles the serialization without exposing uninitialized kernel memory to user space. Therefore, standard KASAN and other bug detectors are sufficient to catch any potential memory safety issues (like out-of-bounds accesses or use-after-free) in this code, and a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch introduces a new netlink attribute `RTA_DEL_REASON` to report the reason for IPv6 route deletions. The deletion reason is passed through the routing subsystem as an explicit `enum rt_del_reason` value (e.g., `RT_DEL_REASON_UNSPEC`, `RT_DEL_REASON_EXPIRED`, `RT_DEL_REASON_RA_WITHDRAWN`). This value is then serialized into the netlink message using `nla_put_u32()`. \n\nSince the `del_reason` is passed by value as an explicit constant in all modified call sites, there is no risk of reading uninitialized memory from stack, heap, or padding bytes. The netlink attribute construction via `nla_put_u32()` safely handles the serialization without exposing uninitialized kernel memory to user space. Therefore, standard KASAN and other bug detectors are sufficient to catch any potential memory safety issues (like out-of-bounds accesses or use-after-free) in this code, and a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)