AI Trajectory


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

Results:
{
  "EnableConfigs": null,
  "FocusSymbols": [
    "napi_kthread_create",
    "napi_stop_kthread",
    "napi_set_threaded",
    "netif_set_threaded",
    "napi_save_config",
    "__netif_napi_del_locked",
    "napi_thread_wait",
    "netdev_napi_exit"
  ],
  "Reasoning": "The patch modifies the core networking NAPI threaded polling mechanism, changing how NAPI kthreads are created, parked, and destroyed to persist across NAPI resets (e.g., link flaps). This is functional core kernel logic that is reachable and should be fuzzed.",
  "WorthFuzzing": true
}

1/1 2026/06/29 20:50 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit e5b1012c5151048af0ac5b5a54baba191c4bbe2b\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Mon Jun 29 20:50:14 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/include/linux/netdevice.h b/include/linux/netdevice.h\nindex 9981d637f8b546..05e430f10aba42 100644\n--- a/include/linux/netdevice.h\n+++ b/include/linux/netdevice.h\n@@ -63,6 +63,7 @@ struct dsa_port;\n struct ip_tunnel_parm_kern;\n struct macsec_context;\n struct macsec_ops;\n+struct napi_struct;\n struct netdev_config;\n struct netdev_name_node;\n struct sd_flow_limit;\n@@ -363,6 +364,10 @@ struct gro_node {\n \tu32\t\t\tcached_napi_id;\n };\n \n+struct napi_thread_ctx {\n+\tstruct napi_struct *napi;\n+};\n+\n /*\n  * Structure for per-NAPI config\n  */\n@@ -371,6 +376,12 @@ struct napi_config {\n \tu64 irq_suspend_timeout;\n \tu32 defer_hard_irqs;\n \tcpumask_t affinity_mask;\n+\t/* thread and thread_ctx mirrors fields of napi_struct when napi_struct\n+\t * is alive. When the napi_struct gets destroyed, napi_config holds the\n+\t * sole reference to the now parked kthread.\n+\t */\n+\tstruct task_struct *thread;\n+\tstruct napi_thread_ctx *thread_ctx;\n \tu8 threaded;\n \tunsigned int napi_id;\n };\n@@ -404,6 +415,7 @@ struct napi_struct {\n \tstruct hrtimer\t\ttimer;\n \t/* all fields past this point are write-protected by netdev_lock */\n \tstruct task_struct\t*thread;\n+\tstruct napi_thread_ctx\t*thread_ctx;\n \tunsigned long\t\tgro_flush_timeout;\n \tunsigned long\t\tirq_suspend_timeout;\n \tu32\t\t\tdefer_hard_irqs;\ndiff --git a/net/core/dev.c b/net/core/dev.c\nindex 4b3d5cfdf6e00f..c81992c929d9d5 100644\n--- a/net/core/dev.c\n+++ b/net/core/dev.c\n@@ -1647,20 +1647,45 @@ static int napi_threaded_poll(void *data);\n \n static int napi_kthread_create(struct napi_struct *n)\n {\n+\tstruct napi_thread_ctx *thread_ctx = NULL;\n \tint err = 0;\n \n+\tif (n-\u003econfig \u0026\u0026 n-\u003econfig-\u003ethread) {\n+\t\tn-\u003ethread_ctx = n-\u003econfig-\u003ethread_ctx;\n+\t\tn-\u003ethread = n-\u003econfig-\u003ethread;\n+\t\tWRITE_ONCE(n-\u003ethread_ctx-\u003enapi, n);\n+\t\tkthread_unpark(n-\u003ethread);\n+\t\treturn 0;\n+\t}\n+\n+\tthread_ctx = kvzalloc_obj(*thread_ctx);\n+\tif (!thread_ctx)\n+\t\treturn -ENOMEM;\n+\n \t/* Create and wake up the kthread once to put it in\n \t * TASK_INTERRUPTIBLE mode to avoid the blocked task\n \t * warning and work with loadavg.\n \t */\n-\tn-\u003ethread = kthread_run(napi_threaded_poll, n, \"napi/%s-%d\",\n+\tthread_ctx-\u003enapi = n;\n+\tn-\u003ethread = kthread_run(napi_threaded_poll, thread_ctx, \"napi/%s-%d\",\n \t\t\t\tn-\u003edev-\u003ename, n-\u003enapi_id);\n \tif (IS_ERR(n-\u003ethread)) {\n \t\terr = PTR_ERR(n-\u003ethread);\n \t\tpr_err(\"kthread_run failed with err %d\\n\", err);\n \t\tn-\u003ethread = NULL;\n+\t\tgoto free_thread_ctx;\n+\t}\n+\tn-\u003ethread_ctx = thread_ctx;\n+\tif (n-\u003econfig) {\n+\t\tn-\u003econfig-\u003ethread = n-\u003ethread;\n+\t\tn-\u003econfig-\u003ethread_ctx = thread_ctx;\n \t}\n \n+\treturn 0;\n+\n+free_thread_ctx:\n+\tkvfree(thread_ctx);\n+\n \treturn err;\n }\n \n@@ -7183,7 +7208,13 @@ static void napi_stop_kthread(struct napi_struct *napi)\n \t}\n \n \tkthread_stop(napi-\u003ethread);\n+\tkvfree(napi-\u003ethread_ctx);\n \tnapi-\u003ethread = NULL;\n+\tnapi-\u003ethread_ctx = NULL;\n+\tif (napi-\u003econfig) {\n+\t\tnapi-\u003econfig-\u003ethread = NULL;\n+\t\tnapi-\u003econfig-\u003ethread_ctx = NULL;\n+\t}\n }\n \n static void napi_set_threaded_state(struct napi_struct *napi,\n@@ -7199,13 +7230,11 @@ static void napi_set_threaded_state(struct napi_struct *napi,\n int napi_set_threaded(struct napi_struct *napi,\n \t\t      enum netdev_napi_threaded threaded)\n {\n-\tif (threaded) {\n-\t\tif (!napi-\u003ethread) {\n-\t\t\tint err = napi_kthread_create(napi);\n+\tif (threaded \u0026\u0026 !napi-\u003ethread) {\n+\t\tint err = napi_kthread_create(napi);\n \n-\t\t\tif (err)\n-\t\t\t\treturn err;\n-\t\t}\n+\t\tif (err)\n+\t\t\treturn err;\n \t}\n \n \tif (napi-\u003econfig)\n@@ -7255,8 +7284,15 @@ int netif_set_threaded(struct net_device *dev,\n \t\tWARN_ON_ONCE(napi_set_threaded(napi, threaded));\n \n \t/* Override the config for all NAPIs even if currently not listed */\n-\tfor (i = 0; i \u003c dev-\u003enum_napi_configs; i++)\n+\tfor (i = 0; i \u003c dev-\u003enum_napi_configs; i++) {\n \t\tdev-\u003enapi_config[i].threaded = threaded;\n+\t\tif (!threaded \u0026\u0026 dev-\u003enapi_config[i].thread) {\n+\t\t\tkthread_stop(dev-\u003enapi_config[i].thread);\n+\t\t\tkvfree(dev-\u003enapi_config[i].thread_ctx);\n+\t\t\tdev-\u003enapi_config[i].thread = NULL;\n+\t\t\tdev-\u003enapi_config[i].thread_ctx = NULL;\n+\t\t}\n+\t}\n \n \treturn err;\n }\n@@ -7501,6 +7537,8 @@ static void napi_save_config(struct napi_struct *n)\n \tn-\u003econfig-\u003edefer_hard_irqs = n-\u003edefer_hard_irqs;\n \tn-\u003econfig-\u003egro_flush_timeout = n-\u003egro_flush_timeout;\n \tn-\u003econfig-\u003eirq_suspend_timeout = n-\u003eirq_suspend_timeout;\n+\tn-\u003econfig-\u003ethread = n-\u003ethread;\n+\tn-\u003econfig-\u003ethread_ctx = n-\u003ethread_ctx;\n \tnapi_hash_del(n);\n }\n \n@@ -7695,6 +7733,21 @@ void __netif_napi_del_locked(struct napi_struct *napi)\n \tif (test_and_clear_bit(NAPI_STATE_HAS_NOTIFIER, \u0026napi-\u003estate))\n \t\tirq_set_affinity_notifier(napi-\u003eirq, NULL);\n \n+\tif (napi-\u003ethread) {\n+\t\tif (napi-\u003econfig) {\n+\t\t\tkthread_park(napi-\u003ethread);\n+\t\t\t/* napi-\u003econfig holds the only reference to the thread\n+\t\t\t * from now on.\n+\t\t\t */\n+\t\t\tnapi-\u003ethread_ctx-\u003enapi = NULL;\n+\t\t} else {\n+\t\t\tkthread_stop(napi-\u003ethread);\n+\t\t\tkvfree(napi-\u003ethread_ctx);\n+\t\t}\n+\t\tnapi-\u003ethread = NULL;\n+\t\tnapi-\u003ethread_ctx = NULL;\n+\t}\n+\n \tif (napi-\u003econfig) {\n \t\tnapi-\u003eindex = -1;\n \t\tnapi-\u003econfig = NULL;\n@@ -7704,11 +7757,6 @@ void __netif_napi_del_locked(struct napi_struct *napi)\n \tnapi_free_frags(napi);\n \n \tgro_cleanup(\u0026napi-\u003egro);\n-\n-\tif (napi-\u003ethread) {\n-\t\tkthread_stop(napi-\u003ethread);\n-\t\tnapi-\u003ethread = NULL;\n-\t}\n }\n EXPORT_SYMBOL(__netif_napi_del_locked);\n \n@@ -7804,11 +7852,18 @@ static int napi_poll(struct napi_struct *n, struct list_head *repoll)\n \treturn work;\n }\n \n-static int napi_thread_wait(struct napi_struct *napi)\n+static struct napi_struct *napi_thread_wait(struct napi_thread_ctx *thread_ctx)\n {\n+\tstruct napi_struct *napi = READ_ONCE(thread_ctx-\u003enapi);\n \tset_current_state(TASK_INTERRUPTIBLE);\n \n \twhile (!kthread_should_stop()) {\n+\t\tif (kthread_should_park()) {\n+\t\t\tkthread_parkme();\n+\t\t\tnapi = READ_ONCE(thread_ctx-\u003enapi);\n+\t\t\t/* Might be awakened for stopping */\n+\t\t\tcontinue;\n+\t\t}\n \t\t/* Testing SCHED_THREADED bit here to make sure the current\n \t\t * kthread owns this napi and could poll on this napi.\n \t\t * Testing SCHED bit is not enough because SCHED bit might be\n@@ -7817,7 +7872,7 @@ static int napi_thread_wait(struct napi_struct *napi)\n \t\tif (test_bit(NAPI_STATE_SCHED_THREADED, \u0026napi-\u003estate)) {\n \t\t\tWARN_ON(!list_empty(\u0026napi-\u003epoll_list));\n \t\t\t__set_current_state(TASK_RUNNING);\n-\t\t\treturn 0;\n+\t\t\treturn napi;\n \t\t}\n \n \t\tschedule();\n@@ -7825,7 +7880,7 @@ static int napi_thread_wait(struct napi_struct *napi)\n \t}\n \t__set_current_state(TASK_RUNNING);\n \n-\treturn -1;\n+\treturn NULL;\n }\n \n static void napi_threaded_poll_loop(struct napi_struct *napi,\n@@ -7882,13 +7937,18 @@ static void napi_threaded_poll_loop(struct napi_struct *napi,\n \n static int napi_threaded_poll(void *data)\n {\n-\tstruct napi_struct *napi = data;\n+\tstruct napi_thread_ctx *thread_ctx = data;\n \tunsigned long last_qs = jiffies;\n+\tstruct napi_struct *napi;\n \tbool want_busy_poll;\n \tbool in_busy_poll;\n \tunsigned long val;\n \n-\twhile (!napi_thread_wait(napi)) {\n+\twhile (1) {\n+\t\tnapi = napi_thread_wait(thread_ctx);\n+\t\tif (!napi)\n+\t\t\tbreak;\n+\n \t\tval = READ_ONCE(napi-\u003estate);\n \n \t\twant_busy_poll = val \u0026 NAPIF_STATE_THREADED_BUSY_POLL;\n@@ -12128,11 +12188,11 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,\n \t\tgoto free_all;\n \tdev-\u003ecfg_pending = dev-\u003ecfg;\n \n-\tdev-\u003enum_napi_configs = maxqs;\n \tnapi_config_sz = array_size(maxqs, sizeof(*dev-\u003enapi_config));\n \tdev-\u003enapi_config = kvzalloc(napi_config_sz, GFP_KERNEL_ACCOUNT);\n \tif (!dev-\u003enapi_config)\n \t\tgoto free_all;\n+\tdev-\u003enum_napi_configs = maxqs;\n \n \tstrscpy(dev-\u003ename, name);\n \tdev-\u003ename_assign_type = name_assign_type;\n@@ -12160,6 +12220,8 @@ EXPORT_SYMBOL(alloc_netdev_mqs);\n \n static void netdev_napi_exit(struct net_device *dev)\n {\n+\tunsigned int i;\n+\n \tif (!list_empty(\u0026dev-\u003enapi_list)) {\n \t\tstruct napi_struct *p, *n;\n \n@@ -12171,6 +12233,12 @@ static void netdev_napi_exit(struct net_device *dev)\n \t\tsynchronize_net();\n \t}\n \n+\tfor (i = 0; i \u003c dev-\u003enum_napi_configs; i++) {\n+\t\tif (dev-\u003enapi_config[i].thread) {\n+\t\t\tkthread_stop(dev-\u003enapi_config[i].thread);\n+\t\t\tkvfree(dev-\u003enapi_config[i].thread_ctx);\n+\t\t}\n+\t}\n \tkvfree(dev-\u003enapi_config);\n }\n \ndiff --git a/tools/testing/selftests/drivers/net/napi_threaded.py b/tools/testing/selftests/drivers/net/napi_threaded.py\nindex f4be72b2145a95..20110fb6942e14 100755\n--- a/tools/testing/selftests/drivers/net/napi_threaded.py\n+++ b/tools/testing/selftests/drivers/net/napi_threaded.py\n@@ -127,6 +127,44 @@ def change_num_queues(cfg, nl) -\u003e None:\n     _assert_napi_threaded_enabled(nl, napi0_id)\n     _assert_napi_threaded_enabled(nl, napi1_id)\n \n+def nic_link_flap(cfg, nl) -\u003e None:\n+    \"\"\"\n+    Test that if threaded is enabled, and NIC goes through\n+    a reset, the kthread stays unchanged across the link flap.\n+    \"\"\"\n+    napis = nl.napi_get({'ifindex': cfg.ifindex}, dump=True)\n+    ksft_ge(len(napis), 2)\n+\n+    napi0_id = napis[0]['id']\n+    napi1_id = napis[1]['id']\n+\n+    _setup_deferred_cleanup(cfg)\n+\n+    # set threaded\n+    _set_threaded_state(cfg, 1)\n+    napis = nl.napi_get({'ifindex': cfg.ifindex}, dump=True)\n+\n+    # check napi threaded is set for both napis\n+    _assert_napi_threaded_enabled(nl, napi0_id)\n+    _assert_napi_threaded_enabled(nl, napi1_id)\n+\n+    pid0 = napis[0].get('pid')\n+    pid1 = napis[1].get('pid')\n+\n+    cmd(f\"ip link set {cfg.ifname} down\")\n+    cmd(f\"ip link set {cfg.ifname} up\")\n+\n+    # re-acquire napi info\n+    napis = nl.napi_get({'ifindex': cfg.ifindex}, dump=True)\n+    ksft_ge(len(napis), 2)\n+\n+    # check napi threaded is set for both napis\n+    _assert_napi_threaded_enabled(nl, napi0_id)\n+    _assert_napi_threaded_enabled(nl, napi1_id)\n+\n+    # check the kthread remains the same\n+    ksft_eq(napis[0].get('pid'), pid0)\n+    ksft_eq(napis[1].get('pid'), pid1)\n \n def main() -\u003e None:\n     \"\"\" Ksft boiler plate main \"\"\"\n@@ -134,7 +172,8 @@ def main() -\u003e None:\n     with NetDrvEnv(__file__, queue_count=2) as cfg:\n         ksft_run([napi_init,\n                   change_num_queues,\n-                  enable_dev_threaded_disable_napi_threaded],\n+                  enable_dev_threaded_disable_napi_threaded,\n+                  nic_link_flap],\n                  args=(cfg, NetdevFamily()))\n     ksft_exit()\n \ndiff --git a/tools/testing/selftests/net/busy_poll_test.sh b/tools/testing/selftests/net/busy_poll_test.sh\nindex 5ec1c85c1623e5..897ce6700601c0 100755\n--- a/tools/testing/selftests/net/busy_poll_test.sh\n+++ b/tools/testing/selftests/net/busy_poll_test.sh\n@@ -124,6 +124,23 @@ test_busypoll_with_napi_threaded()\n \treturn $?\n }\n \n+test_busypoll_with_napi_threaded_link_flap()\n+{\n+\t# Only enable napi threaded poll. Set suspend timeout and prefer busy\n+\t# poll to 0. Run again after a link flap.\n+\ttest_busypoll 0 ${NAPI_THREADED_MODE_BUSY_POLL} 0 || return $?\n+\n+\tip netns exec nssv ip link set dev $NSIM_SV_NAME down\n+\tip netns exec nscl ip link set dev $NSIM_CL_NAME down\n+\n+\tip netns exec nssv ip link set dev $NSIM_SV_NAME up\n+\tip netns exec nscl ip link set dev $NSIM_CL_NAME up\n+\n+\ttest_busypoll 0 ${NAPI_THREADED_MODE_BUSY_POLL} 0\n+\n+\treturn $?\n+}\n+\n ###\n ### Code start\n ###\n@@ -176,6 +193,13 @@ if [ $? -ne 0 ]; then\n \texit 1\n fi\n \n+test_busypoll_with_napi_threaded_link_flap\n+if [ $? -ne 0 ]; then\n+\techo \"test_busypoll_with_napi_threaded_link_flap failed\"\n+\tcleanup_ns\n+\texit 1\n+fi\n+\n echo \"$NSIM_SV_FD:$NSIM_SV_IFIDX\" \u003e $NSIM_DEV_SYS_UNLINK\n \n echo $NSIM_CL_ID \u003e $NSIM_DEV_SYS_DEL\n"
}

2/1 2026/06/29 20:50 agent patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
{
  "EnableConfigs": null,
  "FocusSymbols": [
    "napi_kthread_create",
    "napi_stop_kthread",
    "napi_set_threaded",
    "netif_set_threaded",
    "napi_save_config",
    "__netif_napi_del_locked",
    "napi_thread_wait",
    "netdev_napi_exit"
  ],
  "Reasoning": "The patch modifies the core networking NAPI threaded polling mechanism, changing how NAPI kthreads are created, parked, and destroyed to persist across NAPI resets (e.g., link flaps). This is functional core kernel logic that is reachable and should be fuzzed.",
  "WorthFuzzing": true
}

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

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

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).

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.
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.
   Do not list too generic configs (we already have them enabled). Only list those that
   specifically cover the modified code. 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 e5b1012c5151048af0ac5b5a54baba191c4bbe2b
Author: syz-cluster <triage@syzkaller.com>
Date:   Mon Jun 29 20:50:14 2026 +0000

    syz-cluster: applied patch under review

diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 9981d637f8b546..05e430f10aba42 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -63,6 +63,7 @@ struct dsa_port;
 struct ip_tunnel_parm_kern;
 struct macsec_context;
 struct macsec_ops;
+struct napi_struct;
 struct netdev_config;
 struct netdev_name_node;
 struct sd_flow_limit;
@@ -363,6 +364,10 @@ struct gro_node {
 	u32			cached_napi_id;
 };
 
+struct napi_thread_ctx {
+	struct napi_struct *napi;
+};
+
 /*
  * Structure for per-NAPI config
  */
@@ -371,6 +376,12 @@ struct napi_config {
 	u64 irq_suspend_timeout;
 	u32 defer_hard_irqs;
 	cpumask_t affinity_mask;
+	/* thread and thread_ctx mirrors fields of napi_struct when napi_struct
+	 * is alive. When the napi_struct gets destroyed, napi_config holds the
+	 * sole reference to the now parked kthread.
+	 */
+	struct task_struct *thread;
+	struct napi_thread_ctx *thread_ctx;
 	u8 threaded;
 	unsigned int napi_id;
 };
@@ -404,6 +415,7 @@ struct napi_struct {
 	struct hrtimer		timer;
 	/* all fields past this point are write-protected by netdev_lock */
 	struct task_struct	*thread;
+	struct napi_thread_ctx	*thread_ctx;
 	unsigned long		gro_flush_timeout;
 	unsigned long		irq_suspend_timeout;
 	u32			defer_hard_irqs;
diff --git a/net/core/dev.c b/net/core/dev.c
index 4b3d5cfdf6e00f..c81992c929d9d5 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -1647,20 +1647,45 @@ static int napi_threaded_poll(void *data);
 
 static int napi_kthread_create(struct napi_struct *n)
 {
+	struct napi_thread_ctx *thread_ctx = NULL;
 	int err = 0;
 
+	if (n->config && n->config->thread) {
+		n->thread_ctx = n->config->thread_ctx;
+		n->thread = n->config->thread;
+		WRITE_ONCE(n->thread_ctx->napi, n);
+		kthread_unpark(n->thread);
+		return 0;
+	}
+
+	thread_ctx = kvzalloc_obj(*thread_ctx);
+	if (!thread_ctx)
+		return -ENOMEM;
+
 	/* Create and wake up the kthread once to put it in
 	 * TASK_INTERRUPTIBLE mode to avoid the blocked task
 	 * warning and work with loadavg.
 	 */
-	n->thread = kthread_run(napi_threaded_poll, n, "napi/%s-%d",
+	thread_ctx->napi = n;
+	n->thread = kthread_run(napi_threaded_poll, thread_ctx, "napi/%s-%d",
 				n->dev->name, n->napi_id);
 	if (IS_ERR(n->thread)) {
 		err = PTR_ERR(n->thread);
 		pr_err("kthread_run failed with err %d\n", err);
 		n->thread = NULL;
+		goto free_thread_ctx;
+	}
+	n->thread_ctx = thread_ctx;
+	if (n->config) {
+		n->config->thread = n->thread;
+		n->config->thread_ctx = thread_ctx;
 	}
 
+	return 0;
+
+free_thread_ctx:
+	kvfree(thread_ctx);
+
 	return err;
 }
 
@@ -7183,7 +7208,13 @@ static void napi_stop_kthread(struct napi_struct *napi)
 	}
 
 	kthread_stop(napi->thread);
+	kvfree(napi->thread_ctx);
 	napi->thread = NULL;
+	napi->thread_ctx = NULL;
+	if (napi->config) {
+		napi->config->thread = NULL;
+		napi->config->thread_ctx = NULL;
+	}
 }
 
 static void napi_set_threaded_state(struct napi_struct *napi,
@@ -7199,13 +7230,11 @@ static void napi_set_threaded_state(struct napi_struct *napi,
 int napi_set_threaded(struct napi_struct *napi,
 		      enum netdev_napi_threaded threaded)
 {
-	if (threaded) {
-		if (!napi->thread) {
-			int err = napi_kthread_create(napi);
+	if (threaded && !napi->thread) {
+		int err = napi_kthread_create(napi);
 
-			if (err)
-				return err;
-		}
+		if (err)
+			return err;
 	}
 
 	if (napi->config)
@@ -7255,8 +7284,15 @@ int netif_set_threaded(struct net_device *dev,
 		WARN_ON_ONCE(napi_set_threaded(napi, threaded));
 
 	/* Override the config for all NAPIs even if currently not listed */
-	for (i = 0; i < dev->num_napi_configs; i++)
+	for (i = 0; i < dev->num_napi_configs; i++) {
 		dev->napi_config[i].threaded = threaded;
+		if (!threaded && dev->napi_config[i].thread) {
+			kthread_stop(dev->napi_config[i].thread);
+			kvfree(dev->napi_config[i].thread_ctx);
+			dev->napi_config[i].thread = NULL;
+			dev->napi_config[i].thread_ctx = NULL;
+		}
+	}
 
 	return err;
 }
@@ -7501,6 +7537,8 @@ static void napi_save_config(struct napi_struct *n)
 	n->config->defer_hard_irqs = n->defer_hard_irqs;
 	n->config->gro_flush_timeout = n->gro_flush_timeout;
 	n->config->irq_suspend_timeout = n->irq_suspend_timeout;
+	n->config->thread = n->thread;
+	n->config->thread_ctx = n->thread_ctx;
 	napi_hash_del(n);
 }
 
@@ -7695,6 +7733,21 @@ void __netif_napi_del_locked(struct napi_struct *napi)
 	if (test_and_clear_bit(NAPI_STATE_HAS_NOTIFIER, &napi->state))
 		irq_set_affinity_notifier(napi->irq, NULL);
 
+	if (napi->thread) {
+		if (napi->config) {
+			kthread_park(napi->thread);
+			/* napi->config holds the only reference to the thread
+			 * from now on.
+			 */
+			napi->thread_ctx->napi = NULL;
+		} else {
+			kthread_stop(napi->thread);
+			kvfree(napi->thread_ctx);
+		}
+		napi->thread = NULL;
+		napi->thread_ctx = NULL;
+	}
+
 	if (napi->config) {
 		napi->index = -1;
 		napi->config = NULL;
@@ -7704,11 +7757,6 @@ void __netif_napi_del_locked(struct napi_struct *napi)
 	napi_free_frags(napi);
 
 	gro_cleanup(&napi->gro);
-
-	if (napi->thread) {
-		kthread_stop(napi->thread);
-		napi->thread = NULL;
-	}
 }
 EXPORT_SYMBOL(__netif_napi_del_locked);
 
@@ -7804,11 +7852,18 @@ static int napi_poll(struct napi_struct *n, struct list_head *repoll)
 	return work;
 }
 
-static int napi_thread_wait(struct napi_struct *napi)
+static struct napi_struct *napi_thread_wait(struct napi_thread_ctx *thread_ctx)
 {
+	struct napi_struct *napi = READ_ONCE(thread_ctx->napi);
 	set_current_state(TASK_INTERRUPTIBLE);
 
 	while (!kthread_should_stop()) {
+		if (kthread_should_park()) {
+			kthread_parkme();
+			napi = READ_ONCE(thread_ctx->napi);
+			/* Might be awakened for stopping */
+			continue;
+		}
 		/* Testing SCHED_THREADED bit here to make sure the current
 		 * kthread owns this napi and could poll on this napi.
 		 * Testing SCHED bit is not enough because SCHED bit might be
@@ -7817,7 +7872,7 @@ static int napi_thread_wait(struct napi_struct *napi)
 		if (test_bit(NAPI_STATE_SCHED_THREADED, &napi->state)) {
 			WARN_ON(!list_empty(&napi->poll_list));
 			__set_current_state(TASK_RUNNING);
-			return 0;
+			return napi;
 		}
 
 		schedule();
@@ -7825,7 +7880,7 @@ static int napi_thread_wait(struct napi_struct *napi)
 	}
 	__set_current_state(TASK_RUNNING);
 
-	return -1;
+	return NULL;
 }
 
 static void napi_threaded_poll_loop(struct napi_struct *napi,
@@ -7882,13 +7937,18 @@ static void napi_threaded_poll_loop(struct napi_struct *napi,
 
 static int napi_threaded_poll(void *data)
 {
-	struct napi_struct *napi = data;
+	struct napi_thread_ctx *thread_ctx = data;
 	unsigned long last_qs = jiffies;
+	struct napi_struct *napi;
 	bool want_busy_poll;
 	bool in_busy_poll;
 	unsigned long val;
 
-	while (!napi_thread_wait(napi)) {
+	while (1) {
+		napi = napi_thread_wait(thread_ctx);
+		if (!napi)
+			break;
+
 		val = READ_ONCE(napi->state);
 
 		want_busy_poll = val & NAPIF_STATE_THREADED_BUSY_POLL;
@@ -12128,11 +12188,11 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
 		goto free_all;
 	dev->cfg_pending = dev->cfg;
 
-	dev->num_napi_configs = maxqs;
 	napi_config_sz = array_size(maxqs, sizeof(*dev->napi_config));
 	dev->napi_config = kvzalloc(napi_config_sz, GFP_KERNEL_ACCOUNT);
 	if (!dev->napi_config)
 		goto free_all;
+	dev->num_napi_configs = maxqs;
 
 	strscpy(dev->name, name);
 	dev->name_assign_type = name_assign_type;
@@ -12160,6 +12220,8 @@ EXPORT_SYMBOL(alloc_netdev_mqs);
 
 static void netdev_napi_exit(struct net_device *dev)
 {
+	unsigned int i;
+
 	if (!list_empty(&dev->napi_list)) {
 		struct napi_struct *p, *n;
 
@@ -12171,6 +12233,12 @@ static void netdev_napi_exit(struct net_device *dev)
 		synchronize_net();
 	}
 
+	for (i = 0; i < dev->num_napi_configs; i++) {
+		if (dev->napi_config[i].thread) {
+			kthread_stop(dev->napi_config[i].thread);
+			kvfree(dev->napi_config[i].thread_ctx);
+		}
+	}
 	kvfree(dev->napi_config);
 }
 
diff --git a/tools/testing/selftests/drivers/net/napi_threaded.py b/tools/testing/selftests/drivers/net/napi_threaded.py
index f4be72b2145a95..20110fb6942e14 100755
--- a/tools/testing/selftests/drivers/net/napi_threaded.py
+++ b/tools/testing/selftests/drivers/net/napi_threaded.py
@@ -127,6 +127,44 @@ def change_num_queues(cfg, nl) -> None:
     _assert_napi_threaded_enabled(nl, napi0_id)
     _assert_napi_threaded_enabled(nl, napi1_id)
 
+def nic_link_flap(cfg, nl) -> None:
+    """
+    Test that if threaded is enabled, and NIC goes through
+    a reset, the kthread stays unchanged across the link flap.
+    """
+    napis = nl.napi_get({'ifindex': cfg.ifindex}, dump=True)
+    ksft_ge(len(napis), 2)
+
+    napi0_id = napis[0]['id']
+    napi1_id = napis[1]['id']
+
+    _setup_deferred_cleanup(cfg)
+
+    # set threaded
+    _set_threaded_state(cfg, 1)
+    napis = nl.napi_get({'ifindex': cfg.ifindex}, dump=True)
+
+    # check napi threaded is set for both napis
+    _assert_napi_threaded_enabled(nl, napi0_id)
+    _assert_napi_threaded_enabled(nl, napi1_id)
+
+    pid0 = napis[0].get('pid')
+    pid1 = napis[1].get('pid')
+
+    cmd(f"ip link set {cfg.ifname} down")
+    cmd(f"ip link set {cfg.ifname} up")
+
+    # re-acquire napi info
+    napis = nl.napi_get({'ifindex': cfg.ifindex}, dump=True)
+    ksft_ge(len(napis), 2)
+
+    # check napi threaded is set for both napis
+    _assert_napi_threaded_enabled(nl, napi0_id)
+    _assert_napi_threaded_enabled(nl, napi1_id)
+
+    # check the kthread remains the same
+    ksft_eq(napis[0].get('pid'), pid0)
+    ksft_eq(napis[1].get('pid'), pid1)
 
 def main() -> None:
     """ Ksft boiler plate main """
@@ -134,7 +172,8 @@ def main() -> None:
     with NetDrvEnv(__file__, queue_count=2) as cfg:
         ksft_run([napi_init,
                   change_num_queues,
-                  enable_dev_threaded_disable_napi_threaded],
+                  enable_dev_threaded_disable_napi_threaded,
+                  nic_link_flap],
                  args=(cfg, NetdevFamily()))
     ksft_exit()
 
diff --git a/tools/testing/selftests/net/busy_poll_test.sh b/tools/testing/selftests/net/busy_poll_test.sh
index 5ec1c85c1623e5..897ce6700601c0 100755
--- a/tools/testing/selftests/net/busy_poll_test.sh
+++ b/tools/testing/selftests/net/busy_poll_test.sh
@@ -124,6 +124,23 @@ test_busypoll_with_napi_threaded()
 	return $?
 }
 
+test_busypoll_with_napi_threaded_link_flap()
+{
+	# Only enable napi threaded poll. Set suspend timeout and prefer busy
+	# poll to 0. Run again after a link flap.
+	test_busypoll 0 ${NAPI_THREADED_MODE_BUSY_POLL} 0 || return $?
+
+	ip netns exec nssv ip link set dev $NSIM_SV_NAME down
+	ip netns exec nscl ip link set dev $NSIM_CL_NAME down
+
+	ip netns exec nssv ip link set dev $NSIM_SV_NAME up
+	ip netns exec nscl ip link set dev $NSIM_CL_NAME up
+
+	test_busypoll 0 ${NAPI_THREADED_MODE_BUSY_POLL} 0
+
+	return $?
+}
+
 ###
 ### Code start
 ###
@@ -176,6 +193,13 @@ if [ $? -ne 0 ]; then
 	exit 1
 fi
 
+test_busypoll_with_napi_threaded_link_flap
+if [ $? -ne 0 ]; then
+	echo "test_busypoll_with_napi_threaded_link_flap failed"
+	cleanup_ns
+	exit 1
+fi
+
 echo "$NSIM_SV_FD:$NSIM_SV_IFIDX" > $NSIM_DEV_SYS_UNLINK
 
 echo $NSIM_CL_ID > $NSIM_DEV_SYS_DEL

3/2 2026/06/29 20:50 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 6331
							output: 20
							thoughts: 448
						

Thoughts:
**Analyzing the NAPI Threaded Polling Patch**

Okay, so this patch is digging into the heart of the networking stack, specifically how NAPI threaded polling works within the `net/core/dev.c` and `include/linux/netdevice.h` files. It looks like the main thrust is to refactor how the NAPI kthread is handled—creation, parking, destruction, you name it. The introduction of the `napi_thread_ctx` structure and the modification of the kthread lifecycle are the key changes. The goal seems to be to make the NAPI kthread persistent across resets, like a link flap, by holding onto the thread reference in `napi_config`. That's a clever way to handle that.

Let's dive into the core functions it touches: `napi_kthread_create`, `napi_stop_kthread`, `napi_set_threaded`, `netif_set_threaded`, `napi_save_config`, `__netif_napi_del_locked`, `napi_thread_wait`, `napi_threaded_poll_loop`, `napi_threaded_poll`, `alloc_netdev_mqs` and `netdev_napi_exit`. It's a comprehensive touch on the relevant components, which means it will be functional.

Given that this is core networking code, it's very reachable, and potentially vulnerable. Yep, `WorthFuzzing=true`.

Now, before I sign off, I need to check the configuration options. The base code is in `net/core/dev.c` so it's guaranteed to be in the kernel if `CONFIG_NET` is set. However, `CONFIG_NET` is generic. I wonder if there is a config to specifically enable the NAPI threaded polling mechanism. Let's grep for `napi_set_threaded`, `netif_set_threaded` or `napi_threaded_poll` to be sure.



4/2 2026/06/29 20:50 tool grepper
0m Args:
{
  "Expression": "napi_threaded_poll"
}

Results:
{
  "Output": "include/linux/netdevice.h=3591=struct softnet_data {\n--\ninclude/linux/netdevice.h-3604-\tbool\t\t\tin_net_rx_action;\ninclude/linux/netdevice.h:3605:\tbool\t\t\tin_napi_threaded_poll;\ninclude/linux/netdevice.h-3606-\n--\nnet/core/dev.c=1644=EXPORT_SYMBOL(netdev_notify_peers);\nnet/core/dev.c-1645-\nnet/core/dev.c:1646:static int napi_threaded_poll(void *data);\nnet/core/dev.c-1647-\nnet/core/dev.c=1648=static int napi_kthread_create(struct napi_struct *n)\n--\nnet/core/dev.c-1669-\tthread_ctx-\u003enapi = n;\nnet/core/dev.c:1670:\tn-\u003ethread = kthread_run(napi_threaded_poll, thread_ctx, \"napi/%s-%d\",\nnet/core/dev.c-1671-\t\t\t\tn-\u003edev-\u003ename, n-\u003enapi_id);\n--\nnet/core/dev.c=5311=static void napi_schedule_rps(struct softnet_data *sd)\n--\nnet/core/dev.c-5324-\nnet/core/dev.c:5325:\t\t/* If not called from net_rx_action() or napi_threaded_poll()\nnet/core/dev.c-5326-\t\t * we have to raise NET_RX_SOFTIRQ.\nnet/core/dev.c-5327-\t\t */\nnet/core/dev.c:5328:\t\tif (!mysd-\u003ein_net_rx_action \u0026\u0026 !mysd-\u003ein_napi_threaded_poll)\nnet/core/dev.c-5329-\t\t\t__raise_softirq_irqoff(NET_RX_SOFTIRQ);\n--\nnet/core/dev.c=7855=static struct napi_struct *napi_thread_wait(struct napi_thread_ctx *thread_ctx)\n--\nnet/core/dev.c-7885-\nnet/core/dev.c:7886:static void napi_threaded_poll_loop(struct napi_struct *napi,\nnet/core/dev.c-7887-\t\t\t\t    unsigned long *busy_poll_last_qs)\n--\nnet/core/dev.c-7900-\t\tsd = this_cpu_ptr(\u0026softnet_data);\nnet/core/dev.c:7901:\t\tsd-\u003ein_napi_threaded_poll = true;\nnet/core/dev.c-7902-\n--\nnet/core/dev.c-7906-\nnet/core/dev.c:7907:\t\tsd-\u003ein_napi_threaded_poll = false;\nnet/core/dev.c-7908-\t\tbarrier();\n--\nnet/core/dev.c-7937-\nnet/core/dev.c:7938:static int napi_threaded_poll(void *data)\nnet/core/dev.c-7939-{\n--\nnet/core/dev.c-7963-\nnet/core/dev.c:7964:\t\tnapi_threaded_poll_loop(napi, want_busy_poll ? \u0026last_qs : NULL);\nnet/core/dev.c-7965-\t}\n--\nnet/core/dev.c=13268=static void run_backlog_napi(unsigned int cpu)\n--\nnet/core/dev.c-13271-\nnet/core/dev.c:13272:\tnapi_threaded_poll_loop(\u0026sd-\u003ebacklog, NULL);\nnet/core/dev.c-13273-}\n--\ntools/testing/selftests/net/busy_poller.c=71=static uint64_t cfg_irq_suspend_timeout;\ntools/testing/selftests/net/busy_poller.c:72:static enum netdev_napi_threaded cfg_napi_threaded_poll = NETDEV_NAPI_THREADED_DISABLED;\ntools/testing/selftests/net/busy_poller.c-73-\ntools/testing/selftests/net/busy_poller.c=74=static void usage(const char *filepath)\n--\ntools/testing/selftests/net/busy_poller.c-76-\terror(1, 0,\ntools/testing/selftests/net/busy_poller.c:77:\t      \"Usage: %s -p\u003cport\u003e -b\u003caddr\u003e -m\u003cmax_events\u003e -u\u003cbusy_poll_usecs\u003e -P\u003cprefer_busy_poll\u003e -g\u003cbusy_poll_budget\u003e -o\u003coutfile\u003e -d\u003cdefer_hard_irqs\u003e -r\u003cgro_flu...\ntools/testing/selftests/net/busy_poller.c-78-\t      filepath);\n--\ntools/testing/selftests/net/busy_poller.c=81=static void parse_opts(int argc, char **argv)\n--\ntools/testing/selftests/net/busy_poller.c-175-\ntools/testing/selftests/net/busy_poller.c:176:\t\t\tcfg_napi_threaded_poll = (enum netdev_napi_threaded)tmp;\ntools/testing/selftests/net/busy_poller.c-177-\t\t\tbreak;\n--\ntools/testing/selftests/net/busy_poller.c=226=static void setup_queue(void)\n--\ntools/testing/selftests/net/busy_poller.c-256-\ntools/testing/selftests/net/busy_poller.c:257:\tif (cfg_napi_threaded_poll)\ntools/testing/selftests/net/busy_poller.c:258:\t\tnetdev_napi_set_req_set_threaded(set_req, cfg_napi_threaded_poll);\ntools/testing/selftests/net/busy_poller.c-259-\n"
}

5/2 2026/06/29 20:50 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 8787
							output: 164
							thoughts: 469
						

Thoughts:
**My Assessment of the NAPI Threaded Polling Code Changes**

Okay, so I'm looking at this patch and, from what I can see, the relevant code's all in `net/core/dev.c`. Nothing too specific in terms of configurations for the system; this is core networking stuff. For the `EnableConfigs` aspect, an empty list seems appropriate given the context.

I went through the focus symbols, making sure I understood the functions affected. I initially considered whether to exclude certain functions because of their potential high-frequency usage but on second thought I will include the following functions: `napi_kthread_create`, `napi_stop_kthread`, `napi_set_threaded`, `netif_set_threaded`, `napi_save_config`, `__netif_napi_del_locked`, `napi_thread_wait`, and `netdev_napi_exit`. The other functions are part of critical paths, like device allocation and polling, so I decided to avoid them.

Therefore, the patch appears to be modifying fairly functional, core networking code, specifically the NAPI threaded polling mechanism, which warrants some careful fuzzing. I believe this is a valid and good patch.



6/2 2026/06/29 20:50 tool set-results
0m Args:
{
  "EnableConfigs": [],
  "FocusSymbols": [
    "napi_kthread_create",
    "napi_stop_kthread",
    "napi_set_threaded",
    "netif_set_threaded",
    "napi_save_config",
    "__netif_napi_del_locked",
    "napi_thread_wait",
    "netdev_napi_exit"
  ],
  "Reasoning": "The patch modifies the core networking NAPI threaded polling mechanism, changing how NAPI kthreads are created, parked, and destroyed to persist across NAPI resets (e.g., link flaps). This is functional core kernel logic that is reachable and should be fuzzed.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": null,
  "FocusSymbols": [
    "napi_kthread_create",
    "napi_stop_kthread",
    "napi_set_threaded",
    "netif_set_threaded",
    "napi_save_config",
    "__netif_napi_del_locked",
    "napi_thread_wait",
    "netdev_napi_exit"
  ],
  "Reasoning": "The patch modifies the core networking NAPI threaded polling mechanism, changing how NAPI kthreads are created, parked, and destroyed to persist across NAPI resets (e.g., link flaps). This is functional core kernel logic that is reachable and should be fuzzed.",
  "WorthFuzzing": true
}

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