| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/08/12 18:19 | flow | patch-triage |
1mArgs:null Results: {
"EnableConfigs": [
"NET_DEV_REFCNT_TRACKER",
"KALLSYMS"
],
"FocusSymbols": [
"save_netdev_trace_buffer"
],
"KMSANReasoning": "The patch introduces a debugging mechanism for tracking `net_device` reference count leaks (`CONFIG_NET_DEV_REFCNT_TRACKER`). It records stack traces on `__dev_hold` and `__dev_put` into a statically allocated buffer and dumps them if a device fails to unregister within a timeout. All stack traces are saved using `stack_trace_save`, and only the explicitly initialized number of entries (`nr_entries`) are subsequently read, compared, or copied. No new data structures are exposed or copied to user space, and no conditional logic depends on uninitialized memory. Any potential bugs introduced by this patch (e.g., out-of-bounds accesses in the trace buffer, use-after-free on device pointers, or locking issues) would be effectively caught by standard KASAN and LOCKDEP. There is no risk of uninitialized memory use or info-leaks that would necessitate a dedicated KMSAN fuzzing session.",
"NeedsKMSAN": false,
"Reasoning": "The patch introduces a complex debugging feature for tracking net_device refcounts, involving custom trace buffers, linked lists, RCU, and raw spinlocks. It is not purely decorative as it maintains significant state and could introduce bugs like race conditions or deadlocks.",
"WorthFuzzing": true
} |
| 1/1 | 2026/08/12 18:19 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 65b7d10e6a299229a27fcb3a888acaf47c87a090\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Wed Aug 12 18:19:57 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/include/linux/netdevice.h b/include/linux/netdevice.h\nindex 8840b126979ff..12dc2c1dc0316 100644\n--- a/include/linux/netdevice.h\n+++ b/include/linux/netdevice.h\n@@ -2144,6 +2144,8 @@ enum netdev_reg_state {\n *\n *\tFIXME: cleanup struct net_device such that network protocol info\n *\tmoves out.\n+ *\n+ *\t@netdev_trace_buffer_list: Linked list for debugging refcount leak.\n */\n \n struct net_device {\n@@ -2300,6 +2302,9 @@ struct net_device {\n #if IS_ENABLED(CONFIG_TLS_DEVICE)\n \tconst struct tlsdev_ops *tlsdev_ops;\n #endif\n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+\tstruct list_head\tnetdev_trace_buffer_list;\n+#endif\n \n \tunsigned int\t\toperstate;\n \tunsigned char\t\tlink_mode;\n@@ -3250,6 +3255,7 @@ enum netdev_cmd {\n \tNETDEV_OFFLOAD_XSTATS_REPORT_USED,\n \tNETDEV_OFFLOAD_XSTATS_REPORT_DELTA,\n \tNETDEV_XDP_FEAT_CHANGE,\n+\tNETDEV_DEBUG_UNREGISTER,\n };\n const char *netdev_cmd_to_name(enum netdev_cmd cmd);\n \n@@ -4466,9 +4472,16 @@ static inline bool dev_nit_active(const struct net_device *dev)\n \n void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev);\n \n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+void save_netdev_trace_buffer(struct net_device *dev, int delta);\n+#else\n+static inline void save_netdev_trace_buffer(struct net_device *dev, int delta) { }\n+#endif\n+\n static inline void __dev_put(struct net_device *dev)\n {\n \tif (dev) {\n+\t\tsave_netdev_trace_buffer(dev, -1);\n #ifdef CONFIG_PCPU_DEV_REFCNT\n \t\tthis_cpu_dec(*dev-\u003epcpu_refcnt);\n #else\n@@ -4480,6 +4493,7 @@ static inline void __dev_put(struct net_device *dev)\n static inline void __dev_hold(struct net_device *dev)\n {\n \tif (dev) {\n+\t\tsave_netdev_trace_buffer(dev, 1);\n #ifdef CONFIG_PCPU_DEV_REFCNT\n \t\tthis_cpu_inc(*dev-\u003epcpu_refcnt);\n #else\ndiff --git a/kernel/softirq.c b/kernel/softirq.c\nindex 4425d8dce44b4..c5c194bec36e3 100644\n--- a/kernel/softirq.c\n+++ b/kernel/softirq.c\n@@ -576,6 +576,10 @@ static inline bool lockdep_softirq_start(void) { return false; }\n static inline void lockdep_softirq_end(bool in_hardirq) { }\n #endif\n \n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+static noinline void handle_softirqs(bool ksirqd);\n+#endif\n+\n static void handle_softirqs(bool ksirqd)\n {\n \tunsigned long end = jiffies + MAX_SOFTIRQ_TIME;\ndiff --git a/kernel/workqueue.c b/kernel/workqueue.c\nindex 78068ae8f28a6..d761268686f2d 100644\n--- a/kernel/workqueue.c\n+++ b/kernel/workqueue.c\n@@ -3203,6 +3203,10 @@ static bool manage_workers(struct worker *worker)\n \treturn true;\n }\n \n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+static noinline void process_one_work(struct worker *worker, struct work_struct *work);\n+#endif\n+\n /**\n * process_one_work - process single work\n * @worker: self\ndiff --git a/net/core/dev.c b/net/core/dev.c\nindex af260ff5462a6..86fdaa096aedb 100644\n--- a/net/core/dev.c\n+++ b/net/core/dev.c\n@@ -1875,6 +1875,7 @@ const char *netdev_cmd_to_name(enum netdev_cmd cmd)\n \tN(PRE_CHANGEADDR) N(OFFLOAD_XSTATS_ENABLE) N(OFFLOAD_XSTATS_DISABLE)\n \tN(OFFLOAD_XSTATS_REPORT_USED) N(OFFLOAD_XSTATS_REPORT_DELTA)\n \tN(XDP_FEAT_CHANGE)\n+\tN(DEBUG_UNREGISTER)\n \t}\n #undef N\n \treturn \"UNKNOWN_NETDEV_EVENT\";\n@@ -11582,6 +11583,14 @@ int netdev_refcnt_read(const struct net_device *dev)\n }\n EXPORT_SYMBOL(netdev_refcnt_read);\n \n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+static void dump_netdev_trace_buffer(const struct net_device *dev);\n+static void erase_netdev_trace_buffer(const struct net_device *dev);\n+#else\n+static inline void dump_netdev_trace_buffer(const struct net_device *dev) { }\n+static inline void erase_netdev_trace_buffer(const struct net_device *dev) { }\n+#endif\n+\n int netdev_unregister_timeout_secs __read_mostly = 10;\n \n #define WAIT_REFS_MIN_MSECS 1\n@@ -11655,11 +11664,16 @@ static struct net_device *netdev_wait_allrefs_any(struct list_head *list)\n \n \t\tif (time_after(jiffies, warning_time +\n \t\t\t READ_ONCE(netdev_unregister_timeout_secs) * HZ)) {\n+\t\t\trtnl_lock();\n \t\t\tlist_for_each_entry(dev, list, todo_list) {\n \t\t\t\tpr_emerg(\"unregister_netdevice: waiting for %s to become free. Usage count = %d\\n\",\n \t\t\t\t\t dev-\u003ename, netdev_refcnt_read(dev));\n \t\t\t\tref_tracker_dir_print(\u0026dev-\u003erefcnt_tracker, 10);\n+\t\t\t\tcall_netdevice_notifiers(NETDEV_DEBUG_UNREGISTER, dev);\n+\t\t\t\tdump_netdev_trace_buffer(dev);\n \t\t\t}\n+\t\t\t__rtnl_unlock();\n+\t\t\trcu_barrier();\n \n \t\t\twarning_time = jiffies;\n \t\t}\n@@ -12059,6 +12073,9 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,\n \n \tdev-\u003epriv_len = sizeof_priv;\n \n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+\tINIT_LIST_HEAD(\u0026dev-\u003enetdev_trace_buffer_list);\n+#endif\n \tref_tracker_dir_init(\u0026dev-\u003erefcnt_tracker, 128, \"netdev\");\n #ifdef CONFIG_PCPU_DEV_REFCNT\n \tdev-\u003epcpu_refcnt = alloc_percpu(int);\n@@ -12158,6 +12175,7 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,\n free_pcpu:\n #ifdef CONFIG_PCPU_DEV_REFCNT\n \tfree_percpu(dev-\u003epcpu_refcnt);\n+\terase_netdev_trace_buffer(dev);\n free_dev:\n #endif\n \tkvfree(dev);\n@@ -12226,6 +12244,8 @@ void free_netdev(struct net_device *dev)\n \tfree_percpu(dev-\u003epcpu_refcnt);\n \tdev-\u003epcpu_refcnt = NULL;\n #endif\n+\terase_netdev_trace_buffer(dev);\n+\n \tfree_percpu(dev-\u003ecore_stats);\n \tdev-\u003ecore_stats = NULL;\n \tfree_percpu(dev-\u003exdp_bulkq);\n@@ -13229,6 +13249,12 @@ static struct smp_hotplug_thread backlog_threads = {\n \t.setup\t\t\t= backlog_napi_setup,\n };\n \n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+static void __init net_dev_refcnt_tracker_init(void);\n+#else\n+static void __init net_dev_refcnt_tracker_init(void) { };\n+#endif\n+\n /*\n * This is called single threaded during boot, so no need\n * to take the rtnl semaphore.\n@@ -13237,6 +13263,7 @@ static int __init net_dev_init(void)\n {\n \tint i, rc = -ENOMEM;\n \n+\tnet_dev_refcnt_tracker_init();\n \tBUG_ON(!dev_boot_phase);\n \n \tnet_dev_struct_check();\n@@ -13340,3 +13367,242 @@ static int __init net_dev_init(void)\n }\n \n subsys_initcall(net_dev_init);\n+\n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+\n+#define NETDEV_TRACE_BUFFER_SIZE 32768\n+static struct netdev_trace_buffer {\n+\tstruct list_head list;\n+\tatomic_t count;\n+\tint nr_entries;\n+\tunsigned long entries[20];\n+} netdev_trace_buffer[NETDEV_TRACE_BUFFER_SIZE];\n+static LIST_HEAD(netdev_trace_buffer_list);\n+static DEFINE_RAW_SPINLOCK(netdev_trace_buffer_lock);\n+static bool netdev_trace_buffer_exhausted;\n+static unsigned long start_of_handle_softirqs __ro_after_init;\n+static unsigned long end_of_handle_softirqs __ro_after_init;\n+\n+static int netdev_trace_buffer_init(void)\n+{\n+\tint i;\n+\n+\tfor (i = 0; i \u003c NETDEV_TRACE_BUFFER_SIZE; i++)\n+\t\tlist_add_tail(\u0026netdev_trace_buffer[i].list, \u0026netdev_trace_buffer_list);\n+\treturn 0;\n+}\n+pure_initcall(netdev_trace_buffer_init);\n+\n+static int trim_netdev_trace(unsigned long *entries, int nr_entries)\n+{\n+\tchar buffer[32] = { };\n+\tchar *cp;\n+\tint i;\n+\n+\tfor (i = 0; i \u003c nr_entries; i++) {\n+\t\tsnprintf(buffer, sizeof(buffer) - 1, \"%ps\", (void *)entries[i]);\n+\t\tcp = strchr(buffer, ' ');\n+\t\tif (cp)\n+\t\t\t*cp = '\\0';\n+\t\tif (buffer[0] == 'p') {\n+\t\t\tif (!strcmp(buffer, \"process_one_work\"))\n+\t\t\t\treturn i + 1;\n+\t\t} else if (buffer[0] == 'k') {\n+\t\t\tif (!strcmp(buffer, \"ksys_unshare\"))\n+\t\t\t\treturn i + 1;\n+\t\t} else if (buffer[0] == 's') {\n+\t\t\tif (!strcmp(buffer, \"sock_sendmsg_nosec\") ||\n+\t\t\t !strcmp(buffer, \"sock_recvmsg_nosec\"))\n+\t\t\t\treturn i + 1;\n+\t\t} else if (buffer[0] == '_') {\n+\t\t\tif (!strcmp(buffer, \"__sys_bind\") ||\n+\t\t\t !strcmp(buffer, \"__sock_release\") ||\n+\t\t\t !strcmp(buffer, \"__sys_bpf\"))\n+\t\t\t\treturn i + 1;\n+\t\t} else {\n+\t\t\tif (!strcmp(buffer, \"do_sock_setsockopt\"))\n+\t\t\t\treturn i + 1;\n+\t\t}\n+\t}\n+\treturn nr_entries;\n+}\n+\n+static void dump_netdev_trace_buffer(const struct net_device *dev)\n+{\n+\tstruct netdev_trace_buffer *ptr, *tmp;\n+\tint count, balance = 0, pos = 0;\n+\n+\t/* Trim common traces. */\n+\tlist_for_each_entry_rcu(ptr, \u0026dev-\u003enetdev_trace_buffer_list, list,\n+\t\t\t\t/* list elements can't go away. */ 1) {\n+\t\tptr-\u003enr_entries = trim_netdev_trace(ptr-\u003eentries, ptr-\u003enr_entries);\n+\t}\n+\t/* Merge duplicated entries after trimming common traces. */\n+\tlist_for_each_entry_rcu(ptr, \u0026dev-\u003enetdev_trace_buffer_list, list,\n+\t\t\t\t/* list elements can't go away. */ 1) {\n+\t\t/* Skip empty entries. */\n+\t\tif (!atomic_read(\u0026ptr-\u003ecount))\n+\t\t\tcontinue;\n+\t\tlist_for_each_entry_rcu(tmp, \u0026ptr-\u003elist, list,\n+\t\t\t\t\t/* list elements can't go away. */ 1) {\n+\t\t\tif (ptr-\u003enr_entries != tmp-\u003enr_entries ||\n+\t\t\t memcmp(ptr-\u003eentries, tmp-\u003eentries,\n+\t\t\t\t ptr-\u003enr_entries * sizeof(unsigned long)))\n+\t\t\t\tcontinue;\n+\t\t\t/* Skip empty entries. */\n+\t\t\tcount = atomic_read(\u0026tmp-\u003ecount);\n+\t\t\tif (!count)\n+\t\t\t\tcontinue;\n+\t\t\t/* Move count from non-first entry to first entry. */\n+\t\t\tatomic_add(count, \u0026ptr-\u003ecount);\n+\t\t\tatomic_sub(count, \u0026tmp-\u003ecount);\n+\t\t}\n+\t}\n+\t/* Report all entries for this device. */\n+\tlist_for_each_entry_rcu(ptr, \u0026dev-\u003enetdev_trace_buffer_list, list,\n+\t\t\t\t/* list elements can't go away. */ 1) {\n+\t\t/* Skip empty entries. */\n+\t\tcount = atomic_read(\u0026ptr-\u003ecount);\n+\t\tif (!count)\n+\t\t\tcontinue;\n+\t\t/* Report this entry. It is safe to call cond_resched() because\n+\t\t * this function is called from schedulable context.\n+\t\t */\n+\t\tpos++;\n+\t\tbalance += count;\n+\t\tpr_info(\"Call trace for %s[%d] %+d at\\n\", dev-\u003ename, pos, count);\n+\t\tstack_trace_print(ptr-\u003eentries, ptr-\u003enr_entries, 4);\n+\t\tcond_resched();\n+\t}\n+\tif (!netdev_trace_buffer_exhausted)\n+\t\tpr_info(\"balance as of %s[%d] is %d\\n\", dev-\u003ename, pos, balance);\n+}\n+\n+static void erase_netdev_trace_buffer(const struct net_device *dev)\n+{\n+\tstruct netdev_trace_buffer *ptr;\n+\tunsigned long flags;\n+\n+\t/* This function is called after free_percpu(dev-\u003epcpu_refcnt) was already\n+\t * called, which means that no more __dev_put()/__dev_hold() call can be made.\n+\t * Therefore, no more save_netdev_trace_buffer() call will be made, and we can\n+\t * safely return list elements to netdev_trace_buffer_list.\n+\t */\n+\traw_spin_lock_irqsave(\u0026netdev_trace_buffer_lock, flags);\n+\twhile (!list_empty(\u0026dev-\u003enetdev_trace_buffer_list)) {\n+\t\tptr = list_first_entry(\u0026dev-\u003enetdev_trace_buffer_list, typeof(*ptr), list);\n+\t\tlist_del(\u0026ptr-\u003elist);\n+\t\tlist_add_tail(\u0026ptr-\u003elist, \u0026netdev_trace_buffer_list);\n+\t}\n+\traw_spin_unlock_irqrestore(\u0026netdev_trace_buffer_lock, flags);\n+}\n+\n+void save_netdev_trace_buffer(struct net_device *dev, int delta)\n+{\n+\tstruct netdev_trace_buffer *ptr;\n+\tunsigned long entries[ARRAY_SIZE(ptr-\u003eentries)];\n+\tint nr_entries;\n+\tunsigned long flags;\n+\n+\t/* This function is not NMI-safe. Give up if called from NMI context. */\n+\tif (in_nmi())\n+\t\treturn;\n+\t/* Get stack traces. */\n+\tnr_entries = stack_trace_save(entries, ARRAY_SIZE(ptr-\u003eentries), 1);\n+\t/* Trim traces of process context now if called from softirq context, for\n+\t * we will easily exhaust netdev_trace_buffer_list if we don't trim traces\n+\t * of process context when trying to compare with existing entries.\n+\t *\n+\t * Avoid kallsyms lookup, by using cached address resolved upon boot.\n+\t */\n+\tif (in_softirq()) {\n+\t\tint i;\n+\n+\t\tfor (i = 0; i \u003c nr_entries; i++) {\n+\t\t\tif (entries[i] \u003e= start_of_handle_softirqs \u0026\u0026\n+\t\t\t entries[i] \u003c end_of_handle_softirqs) {\n+\t\t\t\tnr_entries = i + 1;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\t/* Compare with existing entries at best-effort basis. Since duplicated entries\n+\t * created by race condition will be merged when reporting, we don't use lock here.\n+\t */\n+\tlist_for_each_entry_rcu(ptr, \u0026dev-\u003enetdev_trace_buffer_list, list,\n+\t\t\t\t/* list elements can't go away. */ 1) {\n+\t\tif (ptr-\u003enr_entries == nr_entries \u0026\u0026\n+\t\t !memcmp(ptr-\u003eentries, entries, nr_entries * sizeof(unsigned long))) {\n+\t\t\tatomic_add(delta, \u0026ptr-\u003ecount);\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\t/* Add a new entry. We don't re-compare with existing entries with lock held, for\n+\t * duplicated entries created by race condition will be merged when reporting.\n+\t * But we use raw spinlock here in case this function is called with some other\n+\t * raw spinlock already held.\n+\t */\n+\traw_spin_lock_irqsave(\u0026netdev_trace_buffer_lock, flags);\n+\tif (!list_empty(\u0026netdev_trace_buffer_list)) {\n+\t\t/* Remove one entry from netdev_trace_buffer_list and initialize it. */\n+\t\tptr = list_first_entry(\u0026netdev_trace_buffer_list, typeof(*ptr), list);\n+\t\tlist_del(\u0026ptr-\u003elist);\n+\t\tatomic_set(\u0026ptr-\u003ecount, delta);\n+\t\tptr-\u003enr_entries = nr_entries;\n+\t\tmemmove(ptr-\u003eentries, entries, nr_entries * sizeof(unsigned long));\n+\t\t/* Append it in RCU manner, for readers are lockless. */\n+\t\tlist_add_tail_rcu(\u0026ptr-\u003elist, \u0026dev-\u003enetdev_trace_buffer_list);\n+\t} else {\n+\t\tnetdev_trace_buffer_exhausted = true;\n+\t}\n+\traw_spin_unlock_irqrestore(\u0026netdev_trace_buffer_lock, flags);\n+}\n+EXPORT_SYMBOL(save_netdev_trace_buffer);\n+\n+struct timer_completion_struct {\n+\tstruct timer_list timer;\n+\tstruct completion completion;\n+};\n+\n+/* Resolve address of handle_softirqs() and cache it, in order to avoid looking up\n+ * kallsyms every time.\n+ */\n+static void __init netdev_addr_resolve_func(struct timer_list *timer)\n+{\n+\tunsigned long entries[40];\n+\tint nr_entries = stack_trace_save(entries, ARRAY_SIZE(entries), 1);\n+\tchar buffer[KSYM_SYMBOL_LEN] = { };\n+\tunsigned long offset, size;\n+\tchar *cp;\n+\tint i;\n+\n+\tfor (i = 0; i \u003c nr_entries; i++) {\n+\t\tsprint_symbol(buffer, entries[i]);\n+\t\tif (strncmp(buffer, \"handle_softirqs\", 15))\n+\t\t\tcontinue;\n+\t\tcp = strchr(buffer, '+');\n+\t\tif (!cp || sscanf(cp, \"+%lx/%lx\", \u0026offset, \u0026size) != 2)\n+\t\t\tcontinue;\n+\t\tstart_of_handle_softirqs = entries[i] - offset;\n+\t\tend_of_handle_softirqs = start_of_handle_softirqs + size;\n+\t\tbreak;\n+\t}\n+\tcomplete(\u0026container_of(timer, struct timer_completion_struct, timer)-\u003ecompletion);\n+}\n+\n+static void __init net_dev_refcnt_tracker_init(void)\n+{\n+\tstruct timer_completion_struct tc;\n+\n+\ttimer_setup_on_stack(\u0026tc.timer, netdev_addr_resolve_func, 0);\n+\tinit_completion(\u0026tc.completion);\n+\t/* Schedule a call to netdev_addr_resolve_func(). */\n+\tmod_timer(\u0026tc.timer, jiffies);\n+\t/* Wait for netdev_addr_resolve_func() to be called. */\n+\twait_for_completion(\u0026tc.completion);\n+\t/* Wait for netdev_addr_resolve_func() to complete. */\n+\ttimer_delete_sync(\u0026tc.timer);\n+\ttimer_destroy_on_stack(\u0026tc.timer);\n+}\n+\n+#endif\ndiff --git a/net/core/lock_debug.c b/net/core/lock_debug.c\nindex 8a81c54307053..c1a44594c4925 100644\n--- a/net/core/lock_debug.c\n+++ b/net/core/lock_debug.c\n@@ -29,6 +29,7 @@ int netdev_debug_event(struct notifier_block *nb, unsigned long event,\n \tcase NETDEV_DOWN:\n \tcase NETDEV_REBOOT:\n \tcase NETDEV_UNREGISTER:\n+\tcase NETDEV_DEBUG_UNREGISTER:\n \tcase NETDEV_CHANGEMTU:\n \tcase NETDEV_CHANGEADDR:\n \tcase NETDEV_PRE_CHANGEADDR:\ndiff --git a/net/socket.c b/net/socket.c\nindex 63c69a0fa74e1..af7e996364956 100644\n--- a/net/socket.c\n+++ b/net/socket.c\n@@ -698,7 +698,11 @@ struct socket *sock_alloc(void)\n }\n EXPORT_SYMBOL(sock_alloc);\n \n-static void __sock_release(struct socket *sock, struct inode *inode)\n+static\n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+noinline\n+#endif\n+void __sock_release(struct socket *sock, struct inode *inode)\n {\n \tconst struct proto_ops *ops = READ_ONCE(sock-\u003eops);\n \n@@ -770,7 +774,13 @@ static noinline void call_trace_sock_send_length(struct sock *sk, int ret,\n \ttrace_sock_send_length(sk, ret, 0);\n }\n \n-static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)\n+static\n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+noinline\n+#else\n+inline\n+#endif\n+int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)\n {\n \tint ret = INDIRECT_CALL_INET(READ_ONCE(sock-\u003eops)-\u003esendmsg, inet6_sendmsg,\n \t\t\t\t inet_sendmsg, sock, msg,\n@@ -1120,8 +1130,13 @@ static noinline void call_trace_sock_recv_length(struct sock *sk, int ret, int f\n \ttrace_sock_recv_length(sk, ret, flags);\n }\n \n-static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,\n-\t\t\t\t int flags)\n+static\n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+noinline\n+#else\n+inline\n+#endif\n+int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg, int flags)\n {\n \tint ret = INDIRECT_CALL_INET(READ_ONCE(sock-\u003eops)-\u003erecvmsg,\n \t\t\t\t inet6_recvmsg,\n@@ -2624,9 +2639,12 @@ static int copy_msghdr_from_user(struct msghdr *kmsg,\n \treturn err \u003c 0 ? err : 0;\n }\n \n-static int ____sys_sendmsg(struct socket *sock, struct msghdr *msg_sys,\n-\t\t\t unsigned int flags, struct used_address *used_address,\n-\t\t\t unsigned int allowed_msghdr_flags)\n+static\n+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\n+noinline\n+#endif\n+int ____sys_sendmsg(struct socket *sock, struct msghdr *msg_sys, unsigned int flags,\n+\t\t struct used_address *used_address, unsigned int allowed_msghdr_flags)\n {\n \tunsigned char ctl[sizeof(struct cmsghdr) + 20]\n \t\t\t\t__aligned(sizeof(__kernel_size_t));\n"
} |
| 2/1 | 2026/08/12 18:19 | agent | patch-evaluator |
0mModel:best-expensive Args: null Results: {
"EnableConfigs": [
"NET_DEV_REFCNT_TRACKER",
"KALLSYMS"
],
"FocusSymbols": [
"save_netdev_trace_buffer"
],
"Reasoning": "The patch introduces a complex debugging feature for tracking net_device refcounts, involving custom trace buffers, linked lists, RCU, and raw spinlocks. It is not purely decorative as it maintains significant state and could introduce bugs like race conditions or deadlocks.",
"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 65b7d10e6a299229a27fcb3a888acaf47c87a090
Author: syz-cluster <triage@syzkaller.com>
Date: Wed Aug 12 18:19:57 2026 +0000
syz-cluster: applied patch under review
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 8840b126979ff..12dc2c1dc0316 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -2144,6 +2144,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 {
@@ -2300,6 +2302,9 @@ struct net_device {
#if IS_ENABLED(CONFIG_TLS_DEVICE)
const struct tlsdev_ops *tlsdev_ops;
#endif
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+ struct list_head netdev_trace_buffer_list;
+#endif
unsigned int operstate;
unsigned char link_mode;
@@ -3250,6 +3255,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);
@@ -4466,9 +4472,16 @@ static inline bool dev_nit_active(const struct net_device *dev)
void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev);
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+void save_netdev_trace_buffer(struct net_device *dev, int delta);
+#else
+static inline void save_netdev_trace_buffer(struct net_device *dev, int delta) { }
+#endif
+
static inline void __dev_put(struct net_device *dev)
{
if (dev) {
+ save_netdev_trace_buffer(dev, -1);
#ifdef CONFIG_PCPU_DEV_REFCNT
this_cpu_dec(*dev->pcpu_refcnt);
#else
@@ -4480,6 +4493,7 @@ static inline void __dev_put(struct net_device *dev)
static inline void __dev_hold(struct net_device *dev)
{
if (dev) {
+ save_netdev_trace_buffer(dev, 1);
#ifdef CONFIG_PCPU_DEV_REFCNT
this_cpu_inc(*dev->pcpu_refcnt);
#else
diff --git a/kernel/softirq.c b/kernel/softirq.c
index 4425d8dce44b4..c5c194bec36e3 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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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 78068ae8f28a6..d761268686f2d 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -3203,6 +3203,10 @@ static bool manage_workers(struct worker *worker)
return true;
}
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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 af260ff5462a6..86fdaa096aedb 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -1875,6 +1875,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";
@@ -11582,6 +11583,14 @@ int netdev_refcnt_read(const struct net_device *dev)
}
EXPORT_SYMBOL(netdev_refcnt_read);
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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
@@ -11655,11 +11664,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;
}
@@ -12059,6 +12073,9 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
dev->priv_len = sizeof_priv;
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+ 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);
@@ -12158,6 +12175,7 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
free_pcpu:
#ifdef CONFIG_PCPU_DEV_REFCNT
free_percpu(dev->pcpu_refcnt);
+ erase_netdev_trace_buffer(dev);
free_dev:
#endif
kvfree(dev);
@@ -12226,6 +12244,8 @@ void free_netdev(struct net_device *dev)
free_percpu(dev->pcpu_refcnt);
dev->pcpu_refcnt = NULL;
#endif
+ erase_netdev_trace_buffer(dev);
+
free_percpu(dev->core_stats);
dev->core_stats = NULL;
free_percpu(dev->xdp_bulkq);
@@ -13229,6 +13249,12 @@ static struct smp_hotplug_thread backlog_threads = {
.setup = backlog_napi_setup,
};
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+static void __init net_dev_refcnt_tracker_init(void);
+#else
+static void __init net_dev_refcnt_tracker_init(void) { };
+#endif
+
/*
* This is called single threaded during boot, so no need
* to take the rtnl semaphore.
@@ -13237,6 +13263,7 @@ static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
+ net_dev_refcnt_tracker_init();
BUG_ON(!dev_boot_phase);
net_dev_struct_check();
@@ -13340,3 +13367,242 @@ static int __init net_dev_init(void)
}
subsys_initcall(net_dev_init);
+
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+
+#define NETDEV_TRACE_BUFFER_SIZE 32768
+static struct netdev_trace_buffer {
+ struct list_head list;
+ 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_RAW_SPINLOCK(netdev_trace_buffer_lock);
+static bool netdev_trace_buffer_exhausted;
+static unsigned long start_of_handle_softirqs __ro_after_init;
+static unsigned long end_of_handle_softirqs __ro_after_init;
+
+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 int trim_netdev_trace(unsigned long *entries, int nr_entries)
+{
+ char buffer[32] = { };
+ char *cp;
+ int i;
+
+ 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] == 'p') {
+ if (!strcmp(buffer, "process_one_work"))
+ return i + 1;
+ } else 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;
+ }
+ }
+ return nr_entries;
+}
+
+static void dump_netdev_trace_buffer(const struct net_device *dev)
+{
+ struct netdev_trace_buffer *ptr, *tmp;
+ int count, balance = 0, pos = 0;
+
+ /* Trim common traces. */
+ list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list,
+ /* list elements can't go away. */ 1) {
+ ptr->nr_entries = trim_netdev_trace(ptr->entries, ptr->nr_entries);
+ }
+ /* Merge duplicated entries after trimming common traces. */
+ list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list,
+ /* list elements can't go away. */ 1) {
+ /* Skip empty entries. */
+ if (!atomic_read(&ptr->count))
+ continue;
+ list_for_each_entry_rcu(tmp, &ptr->list, list,
+ /* list elements can't go away. */ 1) {
+ if (ptr->nr_entries != tmp->nr_entries ||
+ memcmp(ptr->entries, tmp->entries,
+ ptr->nr_entries * sizeof(unsigned long)))
+ continue;
+ /* Skip empty entries. */
+ count = atomic_read(&tmp->count);
+ if (!count)
+ continue;
+ /* Move count from non-first entry to first entry. */
+ atomic_add(count, &ptr->count);
+ atomic_sub(count, &tmp->count);
+ }
+ }
+ /* Report all entries for this device. */
+ list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list,
+ /* list elements can't go away. */ 1) {
+ /* Skip empty entries. */
+ count = atomic_read(&ptr->count);
+ if (!count)
+ continue;
+ /* Report this entry. It is safe to call cond_resched() because
+ * this function is called from schedulable context.
+ */
+ pos++;
+ balance += 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;
+
+ /* This function is called after free_percpu(dev->pcpu_refcnt) was already
+ * called, which means that no more __dev_put()/__dev_hold() call can be made.
+ * Therefore, no more save_netdev_trace_buffer() call will be made, and we can
+ * safely return list elements to netdev_trace_buffer_list.
+ */
+ raw_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);
+ }
+ raw_spin_unlock_irqrestore(&netdev_trace_buffer_lock, flags);
+}
+
+void save_netdev_trace_buffer(struct net_device *dev, int delta)
+{
+ struct netdev_trace_buffer *ptr;
+ unsigned long entries[ARRAY_SIZE(ptr->entries)];
+ int nr_entries;
+ unsigned long flags;
+
+ /* This function is not NMI-safe. Give up if called from NMI context. */
+ if (in_nmi())
+ return;
+ /* Get stack traces. */
+ nr_entries = stack_trace_save(entries, ARRAY_SIZE(ptr->entries), 1);
+ /* Trim traces of process context now if called from softirq context, for
+ * we will easily exhaust netdev_trace_buffer_list if we don't trim traces
+ * of process context when trying to compare with existing entries.
+ *
+ * Avoid kallsyms lookup, by using cached address resolved upon boot.
+ */
+ if (in_softirq()) {
+ int i;
+
+ for (i = 0; i < nr_entries; i++) {
+ if (entries[i] >= start_of_handle_softirqs &&
+ entries[i] < end_of_handle_softirqs) {
+ nr_entries = i + 1;
+ break;
+ }
+ }
+ }
+ /* Compare with existing entries at best-effort basis. Since duplicated entries
+ * created by race condition will be merged when reporting, we don't use lock here.
+ */
+ 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;
+ }
+ }
+ /* Add a new entry. We don't re-compare with existing entries with lock held, for
+ * duplicated entries created by race condition will be merged when reporting.
+ * But we use raw spinlock here in case this function is called with some other
+ * raw spinlock already held.
+ */
+ raw_spin_lock_irqsave(&netdev_trace_buffer_lock, flags);
+ if (!list_empty(&netdev_trace_buffer_list)) {
+ /* Remove one entry from netdev_trace_buffer_list and initialize it. */
+ ptr = list_first_entry(&netdev_trace_buffer_list, typeof(*ptr), list);
+ list_del(&ptr->list);
+ atomic_set(&ptr->count, delta);
+ ptr->nr_entries = nr_entries;
+ memmove(ptr->entries, entries, nr_entries * sizeof(unsigned long));
+ /* Append it in RCU manner, for readers are lockless. */
+ list_add_tail_rcu(&ptr->list, &dev->netdev_trace_buffer_list);
+ } else {
+ netdev_trace_buffer_exhausted = true;
+ }
+ raw_spin_unlock_irqrestore(&netdev_trace_buffer_lock, flags);
+}
+EXPORT_SYMBOL(save_netdev_trace_buffer);
+
+struct timer_completion_struct {
+ struct timer_list timer;
+ struct completion completion;
+};
+
+/* Resolve address of handle_softirqs() and cache it, in order to avoid looking up
+ * kallsyms every time.
+ */
+static void __init netdev_addr_resolve_func(struct timer_list *timer)
+{
+ unsigned long entries[40];
+ int nr_entries = stack_trace_save(entries, ARRAY_SIZE(entries), 1);
+ char buffer[KSYM_SYMBOL_LEN] = { };
+ unsigned long offset, size;
+ char *cp;
+ int i;
+
+ for (i = 0; i < nr_entries; i++) {
+ sprint_symbol(buffer, entries[i]);
+ if (strncmp(buffer, "handle_softirqs", 15))
+ continue;
+ cp = strchr(buffer, '+');
+ if (!cp || sscanf(cp, "+%lx/%lx", &offset, &size) != 2)
+ continue;
+ start_of_handle_softirqs = entries[i] - offset;
+ end_of_handle_softirqs = start_of_handle_softirqs + size;
+ break;
+ }
+ complete(&container_of(timer, struct timer_completion_struct, timer)->completion);
+}
+
+static void __init net_dev_refcnt_tracker_init(void)
+{
+ struct timer_completion_struct tc;
+
+ timer_setup_on_stack(&tc.timer, netdev_addr_resolve_func, 0);
+ init_completion(&tc.completion);
+ /* Schedule a call to netdev_addr_resolve_func(). */
+ mod_timer(&tc.timer, jiffies);
+ /* Wait for netdev_addr_resolve_func() to be called. */
+ wait_for_completion(&tc.completion);
+ /* Wait for netdev_addr_resolve_func() to complete. */
+ timer_delete_sync(&tc.timer);
+ timer_destroy_on_stack(&tc.timer);
+}
+
+#endif
diff --git a/net/core/lock_debug.c b/net/core/lock_debug.c
index 8a81c54307053..c1a44594c4925 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 63c69a0fa74e1..af7e996364956 100644
--- a/net/socket.c
+++ b/net/socket.c
@@ -698,7 +698,11 @@ struct socket *sock_alloc(void)
}
EXPORT_SYMBOL(sock_alloc);
-static void __sock_release(struct socket *sock, struct inode *inode)
+static
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+noinline
+#endif
+void __sock_release(struct socket *sock, struct inode *inode)
{
const struct proto_ops *ops = READ_ONCE(sock->ops);
@@ -770,7 +774,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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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,
@@ -1120,8 +1130,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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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,
@@ -2624,9 +2639,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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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));
|
| 3/2 | 2026/08/12 18:19 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 8327 output: 23 thoughts: 858 Thoughts: **Fuzzing Decision for Net Device Refcount Tracker** Okay, so I'm looking at a patch that introduces a new debugging feature to track `net_device` refcount leaks. It looks like it utilizes `CONFIG_NET_DEV_REFCNT_TRACKER` and relies on `CONFIG_KALLSYMS` which is standard for these tracing mechanisms. The patch adds a bunch of stuff: a `netdev_trace_buffer_list` to the `net_device` struct, functions like `save_netdev_trace_buffer` (called from hot paths like `__dev_put` and `__dev_hold`), and utility functions like `dump_netdev_trace_buffer` and `erase_netdev_trace_buffer`. It also modifies `netdev_wait_allrefs_any` and adds a initialization function `net_dev_refcnt_tracker_init`. It also adds `noinline` attributes to several core functions. My initial thought is, this seems *purely* for debugging, right? Like `ref_tracker`, `kmemleak`, or `lockdep`. It *seems* like it's not going to make functional changes to the kernel that need fuzzing. However, let's carefully evaluate this. This introduces new logic in the hot paths, notably around `__dev_put` and `__dev_hold`. Then I remember that the prompt says to avoid generic hot-path functions, but is this truly just "purely decorative?" So, on further reflection, this isn't just a tracepoint or a log. This adds a timer, a lock, and manipulates a linked list. This adds state, which is a potential source of errors. Even if the *intention* is debugging, there could be bugs like race conditions, memory leaks or deadlocks. It changes execution flow in these paths. Therefore, from a fuzzing perspective, this patch *is* worth considering. The new functions that warrant extra attention for fuzzing are: `save_netdev_trace_buffer`, `dump_netdev_trace_buffer`, `erase_netdev_trace_buffer`, and `net_dev_refcnt_tracker_init`. Let's confirm that `CONFIG_NET_DEV_REFCNT_TRACKER` is a valid config option using `grepper`. |
| 4/2 | 2026/08/12 18:20 | tool | grepper |
0mArgs:{
"Expression": "NET_DEV_REFCNT_TRACKER"
}Results: {
"Output": "include/linux/netdevice.h=2151=struct net_device {\n--\ninclude/linux/netdevice.h-2304-#endif\ninclude/linux/netdevice.h:2305:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\ninclude/linux/netdevice.h-2306-\tstruct list_head\tnetdev_trace_buffer_list;\n--\ninclude/linux/netdevice.h=4473=void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev);\ninclude/linux/netdevice.h-4474-\ninclude/linux/netdevice.h:4475:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\ninclude/linux/netdevice.h-4476-void save_netdev_trace_buffer(struct net_device *dev, int delta);\n--\ninclude/linux/netdevice.h=4505=static inline void __netdev_tracker_alloc(struct net_device *dev,\n--\ninclude/linux/netdevice.h-4508-{\ninclude/linux/netdevice.h:4509:#ifdef CONFIG_NET_DEV_REFCNT_TRACKER\ninclude/linux/netdevice.h-4510-\tref_tracker_alloc(\u0026dev-\u003erefcnt_tracker, tracker, gfp);\n--\ninclude/linux/netdevice.h=4517=static inline void netdev_tracker_alloc(struct net_device *dev,\n--\ninclude/linux/netdevice.h-4519-{\ninclude/linux/netdevice.h:4520:#ifdef CONFIG_NET_DEV_REFCNT_TRACKER\ninclude/linux/netdevice.h-4521-\trefcount_dec(\u0026dev-\u003erefcnt_tracker.no_tracker);\n--\ninclude/linux/netdevice.h=4526=static inline void netdev_tracker_free(struct net_device *dev,\n--\ninclude/linux/netdevice.h-4528-{\ninclude/linux/netdevice.h:4529:#ifdef CONFIG_NET_DEV_REFCNT_TRACKER\ninclude/linux/netdevice.h-4530-\tref_tracker_free(\u0026dev-\u003erefcnt_tracker, tracker);\n--\ninclude/net/net_trackers.h-5-\ninclude/net/net_trackers.h:6:#ifdef CONFIG_NET_DEV_REFCNT_TRACKER\ninclude/net/net_trackers.h-7-typedef struct ref_tracker *netdevice_tracker;\n--\nkernel/configs/debug.config=40=CONFIG_UBSAN_UNREACHABLE=y\n--\nkernel/configs/debug.config-43-#\nkernel/configs/debug.config:44:CONFIG_NET_DEV_REFCNT_TRACKER=y\nkernel/configs/debug.config-45-CONFIG_NET_NS_REFCNT_TRACKER=y\n--\nkernel/softirq.c=576=static inline void lockdep_softirq_end(bool in_hardirq) { }\n--\nkernel/softirq.c-578-\nkernel/softirq.c:579:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nkernel/softirq.c-580-static noinline void handle_softirqs(bool ksirqd);\n--\nkernel/workqueue.c=3188=static bool manage_workers(struct worker *worker)\n--\nkernel/workqueue.c-3205-\nkernel/workqueue.c:3206:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nkernel/workqueue.c-3207-static noinline void process_one_work(struct worker *worker, struct work_struct *work);\n--\nnet/Kconfig.debug-2-\nnet/Kconfig.debug:3:config NET_DEV_REFCNT_TRACKER\nnet/Kconfig.debug-4-\tbool \"Enable net device refcount tracking\"\n--\nnet/core/dev.c=11584=EXPORT_SYMBOL(netdev_refcnt_read);\nnet/core/dev.c-11585-\nnet/core/dev.c:11586:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/core/dev.c-11587-static void dump_netdev_trace_buffer(const struct net_device *dev);\n--\nnet/core/dev.c=12046=struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,\n--\nnet/core/dev.c-12075-\nnet/core/dev.c:12076:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/core/dev.c-12077-\tINIT_LIST_HEAD(\u0026dev-\u003enetdev_trace_buffer_list);\n--\nnet/core/dev.c=13244=static struct smp_hotplug_thread backlog_threads = {\n--\nnet/core/dev.c-13251-\nnet/core/dev.c:13252:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/core/dev.c-13253-static void __init net_dev_refcnt_tracker_init(void);\n--\nnet/core/dev.c=13369=subsys_initcall(net_dev_init);\nnet/core/dev.c-13370-\nnet/core/dev.c:13371:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/core/dev.c-13372-\n--\nnet/socket.c=701=static\nnet/socket.c:702:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/socket.c-703-noinline\n--\nnet/socket.c=777=static\nnet/socket.c:778:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/socket.c-779-noinline\n--\nnet/socket.c=1133=static\nnet/socket.c:1134:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/socket.c-1135-noinline\n--\nnet/socket.c=2642=static\nnet/socket.c:2643:#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) \u0026\u0026 defined(CONFIG_KALLSYMS)\nnet/socket.c-2644-noinline\n"
} |
| 5/2 | 2026/08/12 18:20 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 11456 output: 32 thoughts: 31 Thoughts: Okay, here's my take on those thoughts, framed as an expert's internal monologue: **Investigating a Network Debug Configuration** Right, so `NET_DEV_REFCNT_TRACKER`… I vaguely remember seeing that in the debug Kconfig options. Let's confirm that it's nestled in `net/Kconfig.debug`, yeah, there it is. Now the interesting bit: what does enabling this thing *actually* depend on? We need to understand its prerequisites. Are there other debug options it piggybacks on? Does it introduce any new kernel features or require specific kernel modules? A good understanding of the dependencies will help me figure out when it's appropriate to enable this and what kind of instrumentation it provides. Okay, let's start digging through the Kconfig to see the `depends on` statements. |
| 6/2 | 2026/08/12 18:20 | tool | read-file |
0mArgs:{
"File": "net/Kconfig.debug",
"FirstLine": 1,
"LineCount": 20
}Results: {
"Contents": " 1:\t# SPDX-License-Identifier: GPL-2.0-only\n 2:\t\n 3:\tconfig NET_DEV_REFCNT_TRACKER\n 4:\t\tbool \"Enable net device refcount tracking\"\n 5:\t\tdepends on DEBUG_KERNEL \u0026\u0026 STACKTRACE_SUPPORT \u0026\u0026 NET\n 6:\t\tselect REF_TRACKER\n 7:\t\tdefault n\n 8:\t\thelp\n 9:\t\t Enable debugging feature to track device references.\n 10:\t\t This adds memory and cpu costs.\n 11:\t\n 12:\tconfig NET_NS_REFCNT_TRACKER\n 13:\t\tbool \"Enable networking namespace refcount tracking\"\n 14:\t\tdepends on DEBUG_KERNEL \u0026\u0026 STACKTRACE_SUPPORT \u0026\u0026 NET\n 15:\t\tselect REF_TRACKER\n 16:\t\tdefault n\n 17:\t\thelp\n 18:\t\t Enable debugging feature to track netns references.\n 19:\t\t This adds memory and cpu costs.\n 20:\t\n"
} |
| 7/2 | 2026/08/12 18:20 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 11947 output: 111 thoughts: 2617 Thoughts: **Analysis of the Net Device Refcount Tracker Patch** Okay, so I've been looking at this patch that introduces the `NET_DEV_REFCNT_TRACKER` config option. It seems like it's adding a debugging feature related to refcount tracking for network devices. The main functions introduced are `save_netdev_trace_buffer`, `dump_netdev_trace_buffer`, `erase_netdev_trace_buffer`, and `net_dev_refcnt_tracker_init`. While the descriptions call it "decorative" initially, after looking at the code I'm not so sure. It seems this is more than just some print statements, it's actually saving stack traces into a custom trace buffer, the `netdev_trace_buffer` array. Now, `save_netdev_trace_buffer` is called within `__dev_put` and `__dev_hold`, which are extremely hot paths. The prompt warned against focusing on hot path functions to prevent skewed test distributions. However, `save_netdev_trace_buffer` is the *only* non-static function introduced by this patch, and it is exported using `EXPORT_SYMBOL`. It's the core logic added by this patch, so it's probably worth it. So, I think this patch is worth fuzzing. The logic in `save_netdev_trace_buffer` does stack trace saves, uses RCU, spinlocks and linked lists. It compares existing entries, adding new entries and the stack trace save itself. It may have bugs (race conditions, memory leaks, deadlocks), so fuzzing is justified. Therefore, my focus is going to be on the `save_netdev_trace_buffer` function. This is because it is the main function added by the patch and it's the only non-static exported symbol, even if it is a hot path. Now, for enabling configurations, I'm going to set `NET_DEV_REFCNT_TRACKER` and `KALLSYMS`. `KALLSYMS` usually is enabled, but it's important to include it. WorthFuzzing=true FocusSymbols = `save_netdev_trace_buffer` EnableConfigs = `NET_DEV_REFCNT_TRACKER`, `KALLSYMS` |
| 8/2 | 2026/08/12 18:20 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"NET_DEV_REFCNT_TRACKER",
"KALLSYMS"
],
"FocusSymbols": [
"save_netdev_trace_buffer"
],
"Reasoning": "The patch introduces a complex debugging feature for tracking net_device refcounts, involving custom trace buffers, linked lists, RCU, and raw spinlocks. It is not purely decorative as it maintains significant state and could introduce bugs like race conditions or deadlocks.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"NET_DEV_REFCNT_TRACKER",
"KALLSYMS"
],
"FocusSymbols": [
"save_netdev_trace_buffer"
],
"Reasoning": "The patch introduces a complex debugging feature for tracking net_device refcounts, involving custom trace buffers, linked lists, RCU, and raw spinlocks. It is not purely decorative as it maintains significant state and could introduce bugs like race conditions or deadlocks.",
"WorthFuzzing": true
} |
| 9/1 | 2026/08/12 18:20 | action | If |
0mArgs:{
"WorthFuzzing": true
}Results: null |
| 10/2 | 2026/08/12 18:20 | agent | kmsan-evaluator |
0mModel:best-expensive Args: null Results: {
"KMSANReasoning": "The patch introduces a debugging mechanism for tracking `net_device` reference count leaks (`CONFIG_NET_DEV_REFCNT_TRACKER`). It records stack traces on `__dev_hold` and `__dev_put` into a statically allocated buffer and dumps them if a device fails to unregister within a timeout. All stack traces are saved using `stack_trace_save`, and only the explicitly initialized number of entries (`nr_entries`) are subsequently read, compared, or copied. No new data structures are exposed or copied to user space, and no conditional logic depends on uninitialized memory. Any potential bugs introduced by this patch (e.g., out-of-bounds accesses in the trace buffer, use-after-free on device pointers, or locking issues) would be effectively caught by standard KASAN and LOCKDEP. There is no risk of uninitialized memory use or info-leaks that would necessitate a dedicated KMSAN fuzzing session.",
"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 65b7d10e6a299229a27fcb3a888acaf47c87a090
Author: syz-cluster <triage@syzkaller.com>
Date: Wed Aug 12 18:19:57 2026 +0000
syz-cluster: applied patch under review
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 8840b126979ff..12dc2c1dc0316 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -2144,6 +2144,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 {
@@ -2300,6 +2302,9 @@ struct net_device {
#if IS_ENABLED(CONFIG_TLS_DEVICE)
const struct tlsdev_ops *tlsdev_ops;
#endif
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+ struct list_head netdev_trace_buffer_list;
+#endif
unsigned int operstate;
unsigned char link_mode;
@@ -3250,6 +3255,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);
@@ -4466,9 +4472,16 @@ static inline bool dev_nit_active(const struct net_device *dev)
void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev);
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+void save_netdev_trace_buffer(struct net_device *dev, int delta);
+#else
+static inline void save_netdev_trace_buffer(struct net_device *dev, int delta) { }
+#endif
+
static inline void __dev_put(struct net_device *dev)
{
if (dev) {
+ save_netdev_trace_buffer(dev, -1);
#ifdef CONFIG_PCPU_DEV_REFCNT
this_cpu_dec(*dev->pcpu_refcnt);
#else
@@ -4480,6 +4493,7 @@ static inline void __dev_put(struct net_device *dev)
static inline void __dev_hold(struct net_device *dev)
{
if (dev) {
+ save_netdev_trace_buffer(dev, 1);
#ifdef CONFIG_PCPU_DEV_REFCNT
this_cpu_inc(*dev->pcpu_refcnt);
#else
diff --git a/kernel/softirq.c b/kernel/softirq.c
index 4425d8dce44b4..c5c194bec36e3 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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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 78068ae8f28a6..d761268686f2d 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -3203,6 +3203,10 @@ static bool manage_workers(struct worker *worker)
return true;
}
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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 af260ff5462a6..86fdaa096aedb 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -1875,6 +1875,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";
@@ -11582,6 +11583,14 @@ int netdev_refcnt_read(const struct net_device *dev)
}
EXPORT_SYMBOL(netdev_refcnt_read);
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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
@@ -11655,11 +11664,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;
}
@@ -12059,6 +12073,9 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
dev->priv_len = sizeof_priv;
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+ 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);
@@ -12158,6 +12175,7 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
free_pcpu:
#ifdef CONFIG_PCPU_DEV_REFCNT
free_percpu(dev->pcpu_refcnt);
+ erase_netdev_trace_buffer(dev);
free_dev:
#endif
kvfree(dev);
@@ -12226,6 +12244,8 @@ void free_netdev(struct net_device *dev)
free_percpu(dev->pcpu_refcnt);
dev->pcpu_refcnt = NULL;
#endif
+ erase_netdev_trace_buffer(dev);
+
free_percpu(dev->core_stats);
dev->core_stats = NULL;
free_percpu(dev->xdp_bulkq);
@@ -13229,6 +13249,12 @@ static struct smp_hotplug_thread backlog_threads = {
.setup = backlog_napi_setup,
};
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+static void __init net_dev_refcnt_tracker_init(void);
+#else
+static void __init net_dev_refcnt_tracker_init(void) { };
+#endif
+
/*
* This is called single threaded during boot, so no need
* to take the rtnl semaphore.
@@ -13237,6 +13263,7 @@ static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
+ net_dev_refcnt_tracker_init();
BUG_ON(!dev_boot_phase);
net_dev_struct_check();
@@ -13340,3 +13367,242 @@ static int __init net_dev_init(void)
}
subsys_initcall(net_dev_init);
+
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+
+#define NETDEV_TRACE_BUFFER_SIZE 32768
+static struct netdev_trace_buffer {
+ struct list_head list;
+ 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_RAW_SPINLOCK(netdev_trace_buffer_lock);
+static bool netdev_trace_buffer_exhausted;
+static unsigned long start_of_handle_softirqs __ro_after_init;
+static unsigned long end_of_handle_softirqs __ro_after_init;
+
+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 int trim_netdev_trace(unsigned long *entries, int nr_entries)
+{
+ char buffer[32] = { };
+ char *cp;
+ int i;
+
+ 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] == 'p') {
+ if (!strcmp(buffer, "process_one_work"))
+ return i + 1;
+ } else 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;
+ }
+ }
+ return nr_entries;
+}
+
+static void dump_netdev_trace_buffer(const struct net_device *dev)
+{
+ struct netdev_trace_buffer *ptr, *tmp;
+ int count, balance = 0, pos = 0;
+
+ /* Trim common traces. */
+ list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list,
+ /* list elements can't go away. */ 1) {
+ ptr->nr_entries = trim_netdev_trace(ptr->entries, ptr->nr_entries);
+ }
+ /* Merge duplicated entries after trimming common traces. */
+ list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list,
+ /* list elements can't go away. */ 1) {
+ /* Skip empty entries. */
+ if (!atomic_read(&ptr->count))
+ continue;
+ list_for_each_entry_rcu(tmp, &ptr->list, list,
+ /* list elements can't go away. */ 1) {
+ if (ptr->nr_entries != tmp->nr_entries ||
+ memcmp(ptr->entries, tmp->entries,
+ ptr->nr_entries * sizeof(unsigned long)))
+ continue;
+ /* Skip empty entries. */
+ count = atomic_read(&tmp->count);
+ if (!count)
+ continue;
+ /* Move count from non-first entry to first entry. */
+ atomic_add(count, &ptr->count);
+ atomic_sub(count, &tmp->count);
+ }
+ }
+ /* Report all entries for this device. */
+ list_for_each_entry_rcu(ptr, &dev->netdev_trace_buffer_list, list,
+ /* list elements can't go away. */ 1) {
+ /* Skip empty entries. */
+ count = atomic_read(&ptr->count);
+ if (!count)
+ continue;
+ /* Report this entry. It is safe to call cond_resched() because
+ * this function is called from schedulable context.
+ */
+ pos++;
+ balance += 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;
+
+ /* This function is called after free_percpu(dev->pcpu_refcnt) was already
+ * called, which means that no more __dev_put()/__dev_hold() call can be made.
+ * Therefore, no more save_netdev_trace_buffer() call will be made, and we can
+ * safely return list elements to netdev_trace_buffer_list.
+ */
+ raw_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);
+ }
+ raw_spin_unlock_irqrestore(&netdev_trace_buffer_lock, flags);
+}
+
+void save_netdev_trace_buffer(struct net_device *dev, int delta)
+{
+ struct netdev_trace_buffer *ptr;
+ unsigned long entries[ARRAY_SIZE(ptr->entries)];
+ int nr_entries;
+ unsigned long flags;
+
+ /* This function is not NMI-safe. Give up if called from NMI context. */
+ if (in_nmi())
+ return;
+ /* Get stack traces. */
+ nr_entries = stack_trace_save(entries, ARRAY_SIZE(ptr->entries), 1);
+ /* Trim traces of process context now if called from softirq context, for
+ * we will easily exhaust netdev_trace_buffer_list if we don't trim traces
+ * of process context when trying to compare with existing entries.
+ *
+ * Avoid kallsyms lookup, by using cached address resolved upon boot.
+ */
+ if (in_softirq()) {
+ int i;
+
+ for (i = 0; i < nr_entries; i++) {
+ if (entries[i] >= start_of_handle_softirqs &&
+ entries[i] < end_of_handle_softirqs) {
+ nr_entries = i + 1;
+ break;
+ }
+ }
+ }
+ /* Compare with existing entries at best-effort basis. Since duplicated entries
+ * created by race condition will be merged when reporting, we don't use lock here.
+ */
+ 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;
+ }
+ }
+ /* Add a new entry. We don't re-compare with existing entries with lock held, for
+ * duplicated entries created by race condition will be merged when reporting.
+ * But we use raw spinlock here in case this function is called with some other
+ * raw spinlock already held.
+ */
+ raw_spin_lock_irqsave(&netdev_trace_buffer_lock, flags);
+ if (!list_empty(&netdev_trace_buffer_list)) {
+ /* Remove one entry from netdev_trace_buffer_list and initialize it. */
+ ptr = list_first_entry(&netdev_trace_buffer_list, typeof(*ptr), list);
+ list_del(&ptr->list);
+ atomic_set(&ptr->count, delta);
+ ptr->nr_entries = nr_entries;
+ memmove(ptr->entries, entries, nr_entries * sizeof(unsigned long));
+ /* Append it in RCU manner, for readers are lockless. */
+ list_add_tail_rcu(&ptr->list, &dev->netdev_trace_buffer_list);
+ } else {
+ netdev_trace_buffer_exhausted = true;
+ }
+ raw_spin_unlock_irqrestore(&netdev_trace_buffer_lock, flags);
+}
+EXPORT_SYMBOL(save_netdev_trace_buffer);
+
+struct timer_completion_struct {
+ struct timer_list timer;
+ struct completion completion;
+};
+
+/* Resolve address of handle_softirqs() and cache it, in order to avoid looking up
+ * kallsyms every time.
+ */
+static void __init netdev_addr_resolve_func(struct timer_list *timer)
+{
+ unsigned long entries[40];
+ int nr_entries = stack_trace_save(entries, ARRAY_SIZE(entries), 1);
+ char buffer[KSYM_SYMBOL_LEN] = { };
+ unsigned long offset, size;
+ char *cp;
+ int i;
+
+ for (i = 0; i < nr_entries; i++) {
+ sprint_symbol(buffer, entries[i]);
+ if (strncmp(buffer, "handle_softirqs", 15))
+ continue;
+ cp = strchr(buffer, '+');
+ if (!cp || sscanf(cp, "+%lx/%lx", &offset, &size) != 2)
+ continue;
+ start_of_handle_softirqs = entries[i] - offset;
+ end_of_handle_softirqs = start_of_handle_softirqs + size;
+ break;
+ }
+ complete(&container_of(timer, struct timer_completion_struct, timer)->completion);
+}
+
+static void __init net_dev_refcnt_tracker_init(void)
+{
+ struct timer_completion_struct tc;
+
+ timer_setup_on_stack(&tc.timer, netdev_addr_resolve_func, 0);
+ init_completion(&tc.completion);
+ /* Schedule a call to netdev_addr_resolve_func(). */
+ mod_timer(&tc.timer, jiffies);
+ /* Wait for netdev_addr_resolve_func() to be called. */
+ wait_for_completion(&tc.completion);
+ /* Wait for netdev_addr_resolve_func() to complete. */
+ timer_delete_sync(&tc.timer);
+ timer_destroy_on_stack(&tc.timer);
+}
+
+#endif
diff --git a/net/core/lock_debug.c b/net/core/lock_debug.c
index 8a81c54307053..c1a44594c4925 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 63c69a0fa74e1..af7e996364956 100644
--- a/net/socket.c
+++ b/net/socket.c
@@ -698,7 +698,11 @@ struct socket *sock_alloc(void)
}
EXPORT_SYMBOL(sock_alloc);
-static void __sock_release(struct socket *sock, struct inode *inode)
+static
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+noinline
+#endif
+void __sock_release(struct socket *sock, struct inode *inode)
{
const struct proto_ops *ops = READ_ONCE(sock->ops);
@@ -770,7 +774,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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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,
@@ -1120,8 +1130,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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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,
@@ -2624,9 +2639,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
+#if defined(CONFIG_NET_DEV_REFCNT_TRACKER) && defined(CONFIG_KALLSYMS)
+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));
|
| 11/3 | 2026/08/12 18:20 | llm | kmsan-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 8324 output: 223 thoughts: 1178 Thoughts: **Analysis of the Net_Device Refcount Tracker Patch for KMSAN Implications**
Okay, let's break this down. We have a patch series designed to debug `net_device` refcount leaks. It uses `CONFIG_NET_DEV_REFCNT_TRACKER` and essentially tracks `__dev_put` and `__dev_hold` calls by saving stack traces into a `netdev_trace_buffer`. This buffer gets dumped if the net_device hangs while unregistered, and a new `NETDEV_DEBUG_UNREGISTER` command is added. Some functions have also been marked `noinline` for better stack trace readability.
My primary concern, as always, is whether this introduces any uninitialized memory risks, which would trigger the need for KMSAN analysis. Let's look at the critical sections.
First, the trace buffer: it's a static buffer, declared as:
```c
#define NETDEV_TRACE_BUFFER_SIZE 32768
static struct netdev_trace_buffer {
struct list_head list;
atomic_t count;
int nr_entries;
unsigned long entries[20];
} netdev_trace_buffer[NETDEV_TRACE_BUFFER_SIZE];
```
Being static, it's zero-initialized by the compiler, which is a good start.
The `save_netdev_trace_buffer` function saves the stack traces. Inside, we see:
```c
unsigned long entries[ARRAY_SIZE(ptr->entries)];
int nr_entries;
...
nr_entries = stack_trace_save(entries, ARRAY_SIZE(ptr->entries), 1);
```
The crucial part is `stack_trace_save`, which is responsible for populating the `entries` array. `stack_trace_save` returns `nr_entries`, which is how many entries were actually written to the `entries` array. Then, there's a comparison:
```c
if (ptr->nr_entries == nr_entries &&
!memcmp(ptr->entries, entries, nr_entries * sizeof(unsigned long))) {
```
This is *safe*. `memcmp` only compares up to the initialized `nr_entries`. The code is careful. Similarly, when copying:
```c
ptr->nr_entries = nr_entries;
memmove(ptr->entries, entries, nr_entries * sizeof(unsigned long));
```
The `memmove` also only copies the initialized part. This is all internal kernel state; nothing is copied to user space, and no control flow depends on uninitialized memory. `stack_trace_save` explicitly initializes the `entries` array up to `nr_entries`.
Let's double-check. The code also uses `snprintf` in the `trim_netdev_trace` function:
```c
char buffer[32] = { };
...
snprintf(buffer, sizeof(buffer) - 1, "%ps", (void *)entries[i]);
```
This is also safe. `snprintf` works just fine on individual entries.
So, in summary: this patch is *purely* for debugging refcount leaks. It does not introduce any new structures that are sent to user space, and it doesn't introduce any new uninitialized memory risks. No bounds checking or data length calculations are modified in a way that would introduce any vulnerabilities.
Therefore, NeedsKMSAN should be *false*.
Let me double-check. The `entries` array in `save_netdev_trace_buffer` is populated by `stack_trace_save`, which returns the number of entries written, `nr_entries`. The rest is indeed uninitialized, but the code only uses `nr_entries` to index and read:
```c
if (ptr->nr_entries == nr_entries &&
!memcmp(ptr->entries, entries, nr_entries * sizeof(unsigned long))) {
```
and
```c
memmove(ptr->entries, entries, nr_entries * sizeof(unsigned long));
```
So, it *only* reads the initialized parts of the arrays.
Finally, the `netdev_addr_resolve_func`:
```c
unsigned long entries[40];
int nr_entries = stack_trace_save(entries, ARRAY_SIZE(entries), 1);
char buffer[KSYM_SYMBOL_LEN] = { };
...
for (i = 0; i < nr_entries; i++) {
sprint_symbol(buffer, entries[i]);
```
Again, the code only reads up to `nr
|
| 12/3 | 2026/08/12 18:21 | tool | set-results |
0mArgs:{
"KMSANReasoning": "The patch introduces a debugging mechanism for tracking `net_device` reference count leaks (`CONFIG_NET_DEV_REFCNT_TRACKER`). It records stack traces on `__dev_hold` and `__dev_put` into a statically allocated buffer and dumps them if a device fails to unregister within a timeout. All stack traces are saved using `stack_trace_save`, and only the explicitly initialized number of entries (`nr_entries`) are subsequently read, compared, or copied. No new data structures are exposed or copied to user space, and no conditional logic depends on uninitialized memory. Any potential bugs introduced by this patch (e.g., out-of-bounds accesses in the trace buffer, use-after-free on device pointers, or locking issues) would be effectively caught by standard KASAN and LOCKDEP. There is no risk of uninitialized memory use or info-leaks that would necessitate a dedicated KMSAN fuzzing session.",
"NeedsKMSAN": false
}Results: {
"KMSANReasoning": "The patch introduces a debugging mechanism for tracking `net_device` reference count leaks (`CONFIG_NET_DEV_REFCNT_TRACKER`). It records stack traces on `__dev_hold` and `__dev_put` into a statically allocated buffer and dumps them if a device fails to unregister within a timeout. All stack traces are saved using `stack_trace_save`, and only the explicitly initialized number of entries (`nr_entries`) are subsequently read, compared, or copied. No new data structures are exposed or copied to user space, and no conditional logic depends on uninitialized memory. Any potential bugs introduced by this patch (e.g., out-of-bounds accesses in the trace buffer, use-after-free on device pointers, or locking issues) would be effectively caught by standard KASAN and LOCKDEP. There is no risk of uninitialized memory use or info-leaks that would necessitate a dedicated KMSAN fuzzing session.",
"NeedsKMSAN": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|