From db002894ba44d6b9425ae34c29caee2ba1ab073a Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Sun, 26 Apr 2026 21:53:38 +0900 Subject: lib/Kconfig.debug: add CONFIG_DEBUG_AID_FOR_SYZBOT option This change is not for upstream. This change is for linux-next only. I want to temporarily enable more debug code for syzbot reports. Link: https://github.com/google/syzkaller/blob/master/docs/syzbot.md#no-custom-patches Signed-off-by: Tetsuo Handa --- lib/Kconfig.debug | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index 8ff5adcfe1e0..152600b96fa6 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -2261,6 +2261,12 @@ config KCOV_SELFTEST On test failure, causes the kernel to panic. Recommended to be enabled, ensuring critical functionality works as intended. +config DEBUG_AID_FOR_SYZBOT + bool "Additional debug code for syzbot" + default n + help + This option is intended for testing by syzbot. + menuconfig RUNTIME_TESTING_MENU bool "Runtime Testing" default y -- cgit v1.2.3 From f607ff4cb6cae7e243b82af3d443fd7133aaaffa Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Mon, 13 Apr 2026 06:49:43 +0900 Subject: locking/lockdep: make lockdep_print_held_locks() always print if CONFIG_DEBUG_AID_FOR_SYZBOT=y This change is not for upstream. This change is for linux-next only. When executing syzkaller testcases, many threads run on few number of CPUs. It is more helpful for syzbot reports to also include locks held by "waiting for CPU" tasks. Signed-off-by: Tetsuo Handa --- kernel/locking/lockdep.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c index 2d4c5bab5af8..f13883162cfe 100644 --- a/kernel/locking/lockdep.c +++ b/kernel/locking/lockdep.c @@ -796,8 +796,10 @@ static void lockdep_print_held_locks(struct task_struct *p) * It's not reliable to print a task's held locks if it's not sleeping * and it's not the current task. */ +#ifndef CONFIG_DEBUG_AID_FOR_SYZBOT if (p != current && task_is_running(p)) return; +#endif for (i = 0; i < depth; i++) { printk(" #%d: ", i); print_lock(p->held_locks + i); -- cgit v1.2.3 From 3b58fbbdf385303fd611abc522579c541e282b8d Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Sun, 26 Apr 2026 21:54:40 +0900 Subject: workqueue: check kvov_mode change inside workqueue This change is not for upstream. This change is for linux-next only. syzbot is hitting WARN_ON(in_task() && kcov_mode_enabled(mode)) condition in kcov_remote_start(). Since current thread is a WQ, let's check whether WQ thread is by error starting with kcov_mode enabled or WQ function is by error changing kcov_mode. It seems that the kernel config which is hitting this problem has CONFIG_PREEMPT=y and CONFIG_PREEMPT_RT=y and CONFIG_PREEMPT_RT_NEEDS_BH_LOCK=n. Therefore, let's check whether this problem depends on CONFIG_PREEMPT_RT=y and CONFIG_PREEMPT_RT_NEEDS_BH_LOCK=n, by enabling CONFIG_PREEMPT_RT via CONFIG_DEBUG_AID_FOR_SYZBOT=y in syzkaller config for linux-next kernels. Link: https://syzkaller.appspot.com/bug?extid=3f51ad7ac3ae57a6fdcc Signed-off-by: Tetsuo Handa --- kernel/workqueue.c | 24 ++++++++++++++++++++++++ lib/Kconfig.debug | 1 + 2 files changed, 25 insertions(+) diff --git a/kernel/workqueue.c b/kernel/workqueue.c index 5f747f241a5f..f6c81e46fc79 100644 --- a/kernel/workqueue.c +++ b/kernel/workqueue.c @@ -3206,6 +3206,9 @@ __acquires(&pool->lock) unsigned long work_data; int lockdep_start_depth, rcu_start_depth; bool bh_draining = pool->flags & POOL_BH_DRAINING; +#ifdef CONFIG_KCOV + unsigned int old_kcov_mode, new_kcov_mode; +#endif #ifdef CONFIG_LOCKDEP /* * It is permissible to free the struct work_struct from @@ -3299,7 +3302,13 @@ __acquires(&pool->lock) */ lockdep_invariant_state(true); trace_workqueue_execute_start(work); +#ifdef CONFIG_KCOV + old_kcov_mode = READ_ONCE(current->kcov_mode); +#endif worker->current_func(work); +#ifdef CONFIG_KCOV + new_kcov_mode = READ_ONCE(current->kcov_mode); +#endif /* * While we must be careful to not use "work" after this, the trace * point will only record its address. @@ -3322,6 +3331,11 @@ __acquires(&pool->lock) debug_show_held_locks(current); dump_stack(); } +#ifdef CONFIG_KCOV + if (unlikely((old_kcov_mode & ~(1 << 30)) != (new_kcov_mode & ~(1 << 30)))) + pr_err("BUG: workqueue function %ps changed kcov_mode from %u to %u\n", + worker->current_func, old_kcov_mode, new_kcov_mode); +#endif /* * The following prevents a kworker from hogging CPU on !PREEMPTION @@ -3413,6 +3427,11 @@ static int worker_thread(void *__worker) struct worker *worker = __worker; struct worker_pool *pool = worker->pool; +#ifdef CONFIG_KCOV + if (unlikely(current->kcov_mode & ~(1 << 30))) + pr_err("BUG: %s started with kcov_mode=%u\n", __func__, current->kcov_mode); +#endif + /* tell the scheduler that this is a workqueue worker */ set_pf_worker(true); woke_up: @@ -3565,6 +3584,11 @@ static int rescuer_thread(void *__rescuer) struct workqueue_struct *wq = rescuer->rescue_wq; bool should_stop; +#ifdef CONFIG_KCOV + if (unlikely(current->kcov_mode & ~(1 << 30))) + pr_err("BUG: %s started with kcov_mode=%u\n", __func__, current->kcov_mode); +#endif + set_user_nice(current, RESCUER_NICE_LEVEL); /* diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index 152600b96fa6..13f3297aa823 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -2264,6 +2264,7 @@ config KCOV_SELFTEST config DEBUG_AID_FOR_SYZBOT bool "Additional debug code for syzbot" default n + select PREEMPT_RT if EXPERT && ARCH_SUPPORTS_RT && !COMPILE_TEST help This option is intended for testing by syzbot. -- cgit v1.2.3 From 8148b8fd9bd90f3a11c5589beca1eb77e787932d Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Sun, 26 Apr 2026 21:58:32 +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 ++++ kernel/softirq.c | 4 + kernel/workqueue.c | 4 + net/core/dev.c | 187 ++++++++++++++++++++++++++++++++++++++++++++++ net/core/lock_debug.c | 1 + net/socket.c | 32 ++++++-- 6 files changed, 236 insertions(+), 7 deletions(-) diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h index 0e1e581efc5a..99fcb5ac45bc 100644 --- a/include/linux/netdevice.h +++ b/include/linux/netdevice.h @@ -2119,6 +2119,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 { @@ -2275,6 +2277,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; @@ -3219,6 +3224,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); @@ -4441,9 +4447,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 @@ -4455,6 +4467,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 diff --git a/kernel/softirq.c b/kernel/softirq.c index 4425d8dce44b..a3bd54de8259 100644 --- a/kernel/softirq.c +++ b/kernel/softirq.c @@ -576,6 +576,10 @@ static inline bool lockdep_softirq_start(void) { return false; } static inline void lockdep_softirq_end(bool in_hardirq) { } #endif +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +static noinline void handle_softirqs(bool ksirqd); +#endif + static void handle_softirqs(bool ksirqd) { unsigned long end = jiffies + MAX_SOFTIRQ_TIME; diff --git a/kernel/workqueue.c b/kernel/workqueue.c index f6c81e46fc79..bdf0879f4caf 100644 --- a/kernel/workqueue.c +++ b/kernel/workqueue.c @@ -3183,6 +3183,10 @@ static bool manage_workers(struct worker *worker) return true; } +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +static noinline void process_one_work(struct worker *worker, struct work_struct *work); +#endif + /** * process_one_work - process single work * @worker: self diff --git a/net/core/dev.c b/net/core/dev.c index 06c195906231..674a0376a6df 100644 --- a/net/core/dev.c +++ b/net/core/dev.c @@ -1874,6 +1874,7 @@ const char *netdev_cmd_to_name(enum netdev_cmd cmd) N(PRE_CHANGEADDR) N(OFFLOAD_XSTATS_ENABLE) N(OFFLOAD_XSTATS_DISABLE) N(OFFLOAD_XSTATS_REPORT_USED) N(OFFLOAD_XSTATS_REPORT_DELTA) N(XDP_FEAT_CHANGE) + N(DEBUG_UNREGISTER) } #undef N return "UNKNOWN_NETDEV_EVENT"; @@ -11555,6 +11556,14 @@ int netdev_refcnt_read(const struct net_device *dev) } EXPORT_SYMBOL(netdev_refcnt_read); +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +static void dump_netdev_trace_buffer(const struct net_device *dev); +static void erase_netdev_trace_buffer(const struct net_device *dev); +#else +static inline void dump_netdev_trace_buffer(const struct net_device *dev) { } +static inline void erase_netdev_trace_buffer(const struct net_device *dev) { } +#endif + int netdev_unregister_timeout_secs __read_mostly = 10; #define WAIT_REFS_MIN_MSECS 1 @@ -11628,11 +11637,16 @@ static struct net_device *netdev_wait_allrefs_any(struct list_head *list) if (time_after(jiffies, warning_time + READ_ONCE(netdev_unregister_timeout_secs) * HZ)) { + rtnl_lock(); list_for_each_entry(dev, list, todo_list) { pr_emerg("unregister_netdevice: waiting for %s to become free. Usage count = %d\n", dev->name, netdev_refcnt_read(dev)); ref_tracker_dir_print(&dev->refcnt_tracker, 10); + call_netdevice_notifiers(NETDEV_DEBUG_UNREGISTER, dev); + dump_netdev_trace_buffer(dev); } + __rtnl_unlock(); + rcu_barrier(); warning_time = jiffies; } @@ -12030,6 +12044,9 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name, dev->priv_len = sizeof_priv; +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + INIT_LIST_HEAD(&dev->netdev_trace_buffer_list); +#endif ref_tracker_dir_init(&dev->refcnt_tracker, 128, "netdev"); #ifdef CONFIG_PCPU_DEV_REFCNT dev->pcpu_refcnt = alloc_percpu(int); @@ -12206,6 +12223,8 @@ void free_netdev(struct net_device *dev) mutex_destroy(&dev->lock); + erase_netdev_trace_buffer(dev); + /* Compatibility with error handling in drivers */ if (dev->reg_state == NETREG_UNINITIALIZED || dev->reg_state == NETREG_DUMMY) { @@ -13312,3 +13331,171 @@ out: } subsys_initcall(net_dev_init); + +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER + +#define NETDEV_TRACE_BUFFER_SIZE 32768 +static struct netdev_trace_buffer { + struct list_head list; + int prev_count; + atomic_t count; + int nr_entries; + unsigned long entries[20]; +} netdev_trace_buffer[NETDEV_TRACE_BUFFER_SIZE]; +static LIST_HEAD(netdev_trace_buffer_list); +static DEFINE_SPINLOCK(netdev_trace_buffer_lock); +static bool netdev_trace_buffer_exhausted; + +static int netdev_trace_buffer_init(void) +{ + int i; + + for (i = 0; i < NETDEV_TRACE_BUFFER_SIZE; i++) + list_add_tail(&netdev_trace_buffer[i].list, &netdev_trace_buffer_list); + return 0; +} +pure_initcall(netdev_trace_buffer_init); + +static void dump_netdev_trace_buffer(const struct net_device *dev) +{ + struct netdev_trace_buffer *ptr; + int count, balance = 0, pos = 0; + + list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list, + /* list elements can't go away. */ 1) { + pos++; + count = atomic_read(&ptr->count); + balance += count; + if (ptr->prev_count == count) + continue; + ptr->prev_count = count; + pr_info("Call trace for %s[%d] %+d at\n", dev->name, pos, count); + stack_trace_print(ptr->entries, ptr->nr_entries, 4); + cond_resched(); + } + if (!netdev_trace_buffer_exhausted) + pr_info("balance as of %s[%d] is %d\n", dev->name, pos, balance); +} + +static void erase_netdev_trace_buffer(const struct net_device *dev) +{ + struct netdev_trace_buffer *ptr; + unsigned long flags; + + spin_lock_irqsave(&netdev_trace_buffer_lock, flags); + while (!list_empty(&dev->netdev_trace_buffer_list)) { + ptr = list_first_entry(&dev->netdev_trace_buffer_list, typeof(*ptr), list); + list_del(&ptr->list); + list_add_tail(&ptr->list, &netdev_trace_buffer_list); + } + spin_unlock_irqrestore(&netdev_trace_buffer_lock, flags); +} + +int trim_netdev_trace(unsigned long *entries, int nr_entries) +{ +#ifdef CONFIG_KALLSYMS + char buffer[32] = { }; + char *cp; + int i; + + if (in_softirq()) { + static unsigned long __data_racy caller; + + if (!caller) { + for (i = 0; i < nr_entries; i++) { + snprintf(buffer, sizeof(buffer) - 1, "%ps", (void *)entries[i]); + cp = strchr(buffer, ' '); + if (cp) + *cp = '\0'; + if (!strcmp(buffer, "handle_softirqs")) { + caller = entries[i]; + break; + } + } + } + for (i = 0; i < nr_entries; i++) + if (entries[i] == caller) + return i + 1; + } else if (current->flags & PF_WQ_WORKER) { + static unsigned long __data_racy caller; + + if (!caller) { + for (i = 0; i < nr_entries; i++) { + snprintf(buffer, sizeof(buffer) - 1, "%ps", (void *)entries[i]); + cp = strchr(buffer, ' '); + if (cp) + *cp = '\0'; + if (!strcmp(buffer, "process_one_work")) { + caller = entries[i]; + break; + } + } + } + for (i = 0; i < nr_entries; i++) + if (entries[i] == caller) + return i + 1; + } else { + for (i = 0; i < nr_entries; i++) { + snprintf(buffer, sizeof(buffer) - 1, "%ps", (void *)entries[i]); + cp = strchr(buffer, ' '); + if (cp) + *cp = '\0'; + if (buffer[0] == 'k') { + if (!strcmp(buffer, "ksys_unshare")) + return i + 1; + } else if (buffer[0] == 's') { + if (!strcmp(buffer, "sock_sendmsg_nosec") || + !strcmp(buffer, "sock_recvmsg_nosec")) + return i + 1; + } else if (buffer[0] == '_') { + if (!strcmp(buffer, "__sys_bind") || + !strcmp(buffer, "__sock_release") || + !strcmp(buffer, "__sys_bpf")) + return i + 1; + } else { + if (!strcmp(buffer, "do_sock_setsockopt")) + return i + 1; + } + } + } +#endif + return nr_entries; +} +EXPORT_SYMBOL(trim_netdev_trace); + +void save_netdev_trace_buffer(struct net_device *dev, int delta) +{ + struct netdev_trace_buffer *ptr; + unsigned long entries[ARRAY_SIZE(ptr->entries)]; + unsigned long nr_entries; + unsigned long flags; + + if (in_nmi()) + return; + nr_entries = stack_trace_save(entries, ARRAY_SIZE(ptr->entries), 1); + nr_entries = trim_netdev_trace(entries, nr_entries); + list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list, + /* list elements can't go away. */ 1) { + if (ptr->nr_entries == nr_entries && + !memcmp(ptr->entries, entries, nr_entries * sizeof(unsigned long))) { + atomic_add(delta, &ptr->count); + return; + } + } + spin_lock_irqsave(&netdev_trace_buffer_lock, flags); + if (!list_empty(&netdev_trace_buffer_list)) { + ptr = list_first_entry(&netdev_trace_buffer_list, typeof(*ptr), list); + list_del(&ptr->list); + ptr->prev_count = 0; + atomic_set(&ptr->count, delta); + ptr->nr_entries = nr_entries; + memmove(ptr->entries, entries, nr_entries * sizeof(unsigned long)); + list_add_tail_rcu(&ptr->list, &dev->netdev_trace_buffer_list); + } else { + netdev_trace_buffer_exhausted = true; + } + spin_unlock_irqrestore(&netdev_trace_buffer_lock, flags); +} +EXPORT_SYMBOL(save_netdev_trace_buffer); + +#endif diff --git a/net/core/lock_debug.c b/net/core/lock_debug.c index 9e9fb25314b9..78d611bb6d1c 100644 --- a/net/core/lock_debug.c +++ b/net/core/lock_debug.c @@ -29,6 +29,7 @@ int netdev_debug_event(struct notifier_block *nb, unsigned long event, case NETDEV_DOWN: case NETDEV_REBOOT: case NETDEV_UNREGISTER: + case NETDEV_DEBUG_UNREGISTER: case NETDEV_CHANGEMTU: case NETDEV_CHANGEADDR: case NETDEV_PRE_CHANGEADDR: diff --git a/net/socket.c b/net/socket.c index 22a412fdec07..12324a9967c5 100644 --- a/net/socket.c +++ b/net/socket.c @@ -710,7 +710,11 @@ struct socket *sock_alloc(void) } EXPORT_SYMBOL(sock_alloc); -static void __sock_release(struct socket *sock, struct inode *inode) +static +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +noinline +#endif +void __sock_release(struct socket *sock, struct inode *inode) { const struct proto_ops *ops = READ_ONCE(sock->ops); @@ -782,7 +786,13 @@ static noinline void call_trace_sock_send_length(struct sock *sk, int ret, trace_sock_send_length(sk, ret, 0); } -static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg) +static +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +noinline +#else +inline +#endif +int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg) { int ret = INDIRECT_CALL_INET(READ_ONCE(sock->ops)->sendmsg, inet6_sendmsg, inet_sendmsg, sock, msg, @@ -1131,8 +1141,13 @@ static noinline void call_trace_sock_recv_length(struct sock *sk, int ret, int f trace_sock_recv_length(sk, ret, flags); } -static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg, - int flags) +static +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +noinline +#else +inline +#endif +int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg, int flags) { int ret = INDIRECT_CALL_INET(READ_ONCE(sock->ops)->recvmsg, inet6_recvmsg, @@ -2638,9 +2653,12 @@ static int copy_msghdr_from_user(struct msghdr *kmsg, return err < 0 ? err : 0; } -static int ____sys_sendmsg(struct socket *sock, struct msghdr *msg_sys, - unsigned int flags, struct used_address *used_address, - unsigned int allowed_msghdr_flags) +static +#ifdef CONFIG_NET_DEV_REFCNT_TRACKER +noinline +#endif +int ____sys_sendmsg(struct socket *sock, struct msghdr *msg_sys, unsigned int flags, + struct used_address *used_address, unsigned int allowed_msghdr_flags) { unsigned char ctl[sizeof(struct cmsghdr) + 20] __aligned(sizeof(__kernel_size_t)); -- cgit v1.2.3 From a363b5ff869c74f292eb80b9755fa64bc0597691 Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Sun, 26 Apr 2026 22:00:18 +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(-) 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 dccd3738c368..f5ec656a9fb6 100644 --- a/include/net/sock.h +++ b/include/net/sock.h @@ -2196,8 +2196,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 674a0376a6df..0e8db7db171b 100644 --- a/net/core/dev.c +++ b/net/core/dev.c @@ -13361,6 +13361,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 From de8a048f0f6a770f0afb7e988f8881c2903dc4ea Mon Sep 17 00:00:00 2001 From: Tetsuo Handa Date: Sun, 19 Apr 2026 19:38:04 +0900 Subject: loop: check lo->lo_backing_file in lo_rw_aio This change is not for upstream. This change is for linux-next only. syzbot is hitting NULL pointer dereference in lo_rw_aio(). Since current thread is a WQ, let's check whether someone is calling __loop_clr_fd() before aio completes. Link: https://syzkaller.appspot.com/bug?extid=cd8a9a308e879a4e2c28 Signed-off-by: Tetsuo Handa --- drivers/block/loop.c | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/drivers/block/loop.c b/drivers/block/loop.c index 0000913f7efc..3ba06b632aa0 100644 --- a/drivers/block/loop.c +++ b/drivers/block/loop.c @@ -74,6 +74,14 @@ struct loop_device { struct gendisk *lo_disk; struct mutex lo_mutex; bool idr_visible; + +#ifdef CONFIG_DEBUG_AID_FOR_SYZBOT + pid_t clear_pid; + char clear_comm[TASK_COMM_LEN]; + unsigned long clear_jiffies; + int clear_nr_entries; + unsigned long clear_entries[20]; +#endif }; struct loop_cmd { @@ -94,6 +102,26 @@ static DEFINE_IDR(loop_index_idr); static DEFINE_MUTEX(loop_ctl_mutex); static DEFINE_MUTEX(loop_validate_mutex); +static struct file *check_file_not_null(struct loop_device *lo) +{ + struct file *file = READ_ONCE(lo->lo_backing_file); + +#ifdef CONFIG_DEBUG_AID_FOR_SYZBOT + if (!file) + pr_err("file == NULL\n"); + else if (IS_ERR(file)) + pr_err("IS_ERR(file)\n"); + else + return file; + if (lo->clear_nr_entries) { + pr_err("Last __loop_clr_fd() call was %lu jiffies ago by %s (%d)\n", + jiffies - lo->clear_jiffies, lo->clear_comm, lo->clear_pid); + stack_trace_print(lo->clear_entries, lo->clear_nr_entries, 4); + } +#endif + return file; +} + /** * loop_global_lock_killable() - take locks for safe loop_validate_file() test * @@ -250,7 +278,7 @@ static int lo_fallocate(struct loop_device *lo, struct request *rq, loff_t pos, * We use fallocate to manipulate the space mappings used by the image * a.k.a. discard/zerorange. */ - struct file *file = lo->lo_backing_file; + struct file *file = check_file_not_null(lo); int ret; mode |= FALLOC_FL_KEEP_SIZE; @@ -345,7 +373,7 @@ static int lo_rw_aio(struct loop_device *lo, struct loop_cmd *cmd, struct bio_vec *bvec; struct request *rq = blk_mq_rq_from_pdu(cmd); struct bio *bio = rq->bio; - struct file *file = lo->lo_backing_file; + struct file *file = check_file_not_null(lo); struct bio_vec tmp; unsigned int offset; unsigned int nr_bvec; @@ -1118,6 +1146,13 @@ static void __loop_clr_fd(struct loop_device *lo) struct file *filp; gfp_t gfp = lo->old_gfp_mask; +#ifdef CONFIG_DEBUG_AID_FOR_SYZBOT + lo->clear_pid = current->pid; + get_task_comm(lo->clear_comm, current); + lo->clear_jiffies = jiffies; + lo->clear_nr_entries = stack_trace_save(lo->clear_entries, + ARRAY_SIZE(lo->clear_entries), 0); +#endif spin_lock_irq(&lo->lo_lock); filp = lo->lo_backing_file; lo->lo_backing_file = NULL; -- cgit v1.2.3