From bfe347a0819f2fb26d0c6d2fecc3b92c09a97ea0 Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Mon, 29 Jun 2026 20:26:36 +0900 Subject: net: update dev_put()/dev_hold() debugging This change is not for upstream. This change is for linux-next only. syzbot is still reporting unregister_netdevice: waiting for DEV to become free problem. Since commit 4c6c11ea0f7b ("net: refine dev_put()/dev_hold() debugging") is not sufficient for me, let's try to report all locations which called dev_put()/dev_hold(), with a hope that we can find some hints for locations where dev_put() is missing. Signed-off-by: Tetsuo Handa --- include/linux/netdevice.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'include') diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h index 9981d637f8b5..987c3c87f14f 100644 --- a/include/linux/netdevice.h +++ b/include/linux/netdevice.h @@ -2142,6 +2142,8 @@ enum netdev_reg_state { * * FIXME: cleanup struct net_device such that network protocol info * moves out. + * + * @netdev_trace_buffer_list: Linked list for debugging refcount leak. */ struct net_device { @@ -2298,6 +2300,9 @@ struct net_device { #if IS_ENABLED(CONFIG_TLS_DEVICE) const struct tlsdev_ops *tlsdev_ops; #endif +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + struct list_head netdev_trace_buffer_list; +#endif unsigned int operstate; unsigned char link_mode; @@ -3248,6 +3253,7 @@ enum netdev_cmd { NETDEV_OFFLOAD_XSTATS_REPORT_USED, NETDEV_OFFLOAD_XSTATS_REPORT_DELTA, NETDEV_XDP_FEAT_CHANGE, + NETDEV_DEBUG_UNREGISTER, }; const char *netdev_cmd_to_name(enum netdev_cmd cmd); @@ -4469,9 +4475,15 @@ static inline bool dev_nit_active(const struct net_device *dev) void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev); +void save_netdev_trace_buffer(struct net_device *dev, int delta); +int trim_netdev_trace(unsigned long *entries, int nr_entries); + static inline void __dev_put(struct net_device *dev) { if (dev) { +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + save_netdev_trace_buffer(dev, -1); +#endif #ifdef CONFIG_PCPU_DEV_REFCNT this_cpu_dec(*dev->pcpu_refcnt); #else @@ -4483,6 +4495,9 @@ static inline void __dev_put(struct net_device *dev) static inline void __dev_hold(struct net_device *dev) { if (dev) { +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + save_netdev_trace_buffer(dev, 1); +#endif #ifdef CONFIG_PCPU_DEV_REFCNT this_cpu_inc(*dev->pcpu_refcnt); #else -- cgit v1.2.3 From 736de390972c25b87fe194540d18cab1cb0d58dc Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Mon, 29 Jun 2026 20:36:46 +0900 Subject: net: add "struct dst_entry" debugging This change is not for upstream. This change is for linux-next only. syzbot is reporting "struct dst_entry" leaks. unregister_netdevice: waiting for lo to become free. Usage count = 2 ref_tracker: netdev@ffff88807a79a630 has 1/1 users at __netdev_tracker_alloc include/linux/netdevice.h:4418 [inline] netdev_hold include/linux/netdevice.h:4447 [inline] dst_init+0xe6/0x490 net/core/dst.c:52 dst_alloc+0x12a/0x170 net/core/dst.c:94 rt_dst_alloc net/ipv4/route.c:1651 [inline] __mkroute_output net/ipv4/route.c:2655 [inline] (...snipped...) Let's try to report all trace hold/release calls. Signed-off-by: Tetsuo Handa --- include/net/dst.h | 22 +++++++++++-- include/net/sock.h | 8 +++-- net/bridge/br_nf_core.c | 1 + net/core/dev.c | 1 + net/core/dst.c | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 4 deletions(-) (limited to 'include') diff --git a/include/net/dst.h b/include/net/dst.h index 307073eae7f8..10b73fb45117 100644 --- a/include/net/dst.h +++ b/include/net/dst.h @@ -95,8 +95,19 @@ struct dst_entry { #ifdef CONFIG_64BIT struct lwtunnel_state *lwtstate; #endif +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + atomic_t dst_trace_seq; +#endif }; +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +void save_dst_trace_buffer(struct dst_entry *dst, int delta); +void dump_dst_trace_buffer(const struct net_device *dev); +#else +static inline void save_dst_trace_buffer(struct dst_entry *dst, int delta) { }; +static inline void dump_dst_trace_buffer(const struct net_device *dev) { }; +#endif + struct dst_metrics { u32 metrics[RTAX_MAX]; refcount_t refcnt; @@ -244,7 +255,10 @@ static inline void dst_hold(struct dst_entry *dst) * the placement of __rcuref in struct dst_entry */ BUILD_BUG_ON(offsetof(struct dst_entry, __rcuref) & 63); - WARN_ON(!rcuref_get(&dst->__rcuref)); + if (!rcuref_get(&dst->__rcuref)) + WARN_ON(1); + else + save_dst_trace_buffer(dst, 1); } static inline void dst_use_noref(struct dst_entry *dst, unsigned long time) @@ -308,7 +322,11 @@ static inline void skb_dst_copy(struct sk_buff *nskb, const struct sk_buff *oskb */ static inline bool dst_hold_safe(struct dst_entry *dst) { - return rcuref_get(&dst->__rcuref); + const bool ret = rcuref_get(&dst->__rcuref); + + if (ret) + save_dst_trace_buffer(dst, 1); + return ret; } /** diff --git a/include/net/sock.h b/include/net/sock.h index 51185222aac2..a043fd422089 100644 --- a/include/net/sock.h +++ b/include/net/sock.h @@ -2206,8 +2206,12 @@ sk_dst_get(const struct sock *sk) rcu_read_lock(); dst = rcu_dereference(sk->sk_dst_cache); - if (dst && !rcuref_get(&dst->__rcuref)) - dst = NULL; + if (dst) { + if (!rcuref_get(&dst->__rcuref)) + dst = NULL; + else + save_dst_trace_buffer(dst, 1); + } rcu_read_unlock(); return dst; } diff --git a/net/bridge/br_nf_core.c b/net/bridge/br_nf_core.c index a8c67035e23c..5b3a85b4be8f 100644 --- a/net/bridge/br_nf_core.c +++ b/net/bridge/br_nf_core.c @@ -70,6 +70,7 @@ void br_netfilter_rtable_init(struct net_bridge *br) struct rtable *rt = &br->fake_rtable; rcuref_init(&rt->dst.__rcuref, 1); + save_dst_trace_buffer(&rt->dst, 1); rt->dst.dev = br->dev; dst_init_metrics(&rt->dst, br->metrics, false); dst_metric_set(&rt->dst, RTAX_MTU, br->dev->mtu); diff --git a/net/core/dev.c b/net/core/dev.c index 4d47ef74d6aa..209af2f322ba 100644 --- a/net/core/dev.c +++ b/net/core/dev.c @@ -13381,6 +13381,7 @@ static void dump_netdev_trace_buffer(const struct net_device *dev) struct netdev_trace_buffer *ptr; int count, balance = 0, pos = 0; + dump_dst_trace_buffer(dev); list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list, /* list elements can't go away. */ 1) { pos++; diff --git a/net/core/dst.c b/net/core/dst.c index 092861133023..45452fa62e24 100644 --- a/net/core/dst.c +++ b/net/core/dst.c @@ -44,6 +44,82 @@ const struct dst_metrics dst_default_metrics = { }; EXPORT_SYMBOL(dst_default_metrics); +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + +#define DST_TRACE_BUFFER_SIZE 8192 +static struct dst_trace_buffer { + struct dst_entry *dst; // no-ref + struct net_device *ndev; // no-ref + int seq; + int delta; + u32 caller_id; + int nr_entries; + unsigned long entries[20]; +} dst_trace_buffer[DST_TRACE_BUFFER_SIZE]; +static bool dst_trace_buffer_exhausted; + +void dump_dst_trace_buffer(const struct net_device *ndev) +{ + struct dst_trace_buffer *ptr; + int count, balance = 0; + int i; + + for (i = 0; i < DST_TRACE_BUFFER_SIZE; i++) { + ptr = &dst_trace_buffer[i]; + if (!ptr->dst || ptr->ndev != ndev) + continue; + count = ptr->delta; + balance += count; + pr_info("Call trace for %s@%p[%d] %+d %c%u at\n", ndev->name, ptr->dst, ptr->seq, + count, ptr->caller_id & 0x80000000 ? 'C' : 'T', + ptr->caller_id & ~0x80000000); + stack_trace_print(ptr->entries, ptr->nr_entries, 4); + } + if (!dst_trace_buffer_exhausted) + pr_info("balance for %s@dst_entry is %d\n", ndev->name, balance); + else + pr_info("balance for %s@dst_entry is unknown\n", ndev->name); +} + +static void erase_dst_trace_buffer(struct dst_entry *dst) +{ + int i; + + for (i = 0; i < DST_TRACE_BUFFER_SIZE; i++) + if (dst_trace_buffer[i].dst == dst) + dst_trace_buffer[i].dst = NULL; +} + +void save_dst_trace_buffer(struct dst_entry *dst, int delta) +{ + struct dst_trace_buffer *ptr; + unsigned long entries[ARRAY_SIZE(ptr->entries)]; + unsigned long nr_entries; + int i; + + if (!dst->dev) + return; + nr_entries = stack_trace_save(entries, ARRAY_SIZE(ptr->entries), 1); + nr_entries = trim_netdev_trace(entries, nr_entries); + for (i = 0; i < DST_TRACE_BUFFER_SIZE; i++) { + ptr = &dst_trace_buffer[i]; + if (!ptr->dst && !cmpxchg(&ptr->dst, NULL, dst)) { + ptr->ndev = dst->dev; + ptr->seq = atomic_inc_return(&dst->dst_trace_seq); + ptr->delta = delta; + ptr->caller_id = in_task() ? task_pid_nr(current) : + 0x80000000 + raw_smp_processor_id(); + ptr->nr_entries = nr_entries; + memmove(ptr->entries, entries, nr_entries * sizeof(unsigned long)); + return; + } + } + dst_trace_buffer_exhausted = true; +} +EXPORT_SYMBOL(save_dst_trace_buffer); + +#endif + void dst_init(struct dst_entry *dst, struct dst_ops *ops, struct net_device *dev, int initial_obsolete, unsigned short flags) @@ -67,6 +143,7 @@ void dst_init(struct dst_entry *dst, struct dst_ops *ops, #endif dst->lwtstate = NULL; rcuref_init(&dst->__rcuref, 1); + save_dst_trace_buffer(dst, 1); INIT_LIST_HEAD(&dst->rt_uncached); dst->rt_uncached_list = NULL; dst->__use = 0; @@ -116,6 +193,10 @@ static void dst_destroy(struct dst_entry *dst) lwtstate_put(dst->lwtstate); +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + erase_dst_trace_buffer(dst); +#endif + if (dst->flags & DST_METADATA) metadata_dst_free((struct metadata_dst *)dst); else @@ -165,6 +246,8 @@ static void dst_count_dec(struct dst_entry *dst) void dst_release(struct dst_entry *dst) { + if (dst) + save_dst_trace_buffer(dst, -1); if (dst && rcuref_put(&dst->__rcuref)) { #ifdef CONFIG_DST_CACHE if (dst->flags & DST_METADATA) { @@ -182,6 +265,8 @@ EXPORT_SYMBOL(dst_release); void dst_release_immediate(struct dst_entry *dst) { + if (dst) + save_dst_trace_buffer(dst, -1); if (dst && rcuref_put(&dst->__rcuref)) { dst_count_dec(dst); dst_destroy(dst); -- cgit v1.2.3