[PATCH RFC] usb: gadget: f_tcm: Fix circular locking dependency in TPG creation

"syzbot" <[email protected]> Thu, 30 Jul 2026 10:54:04 +0000 (UTC)
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
A lockdep splat indicates a circular locking dependency involving
`opts->dep_lock`, `inode_lock` (on configfs directories), and
`gadget_subsys.su_mutex`:

======================================================
WARNING: possible circular locking dependency detected
...
syz.0.18/6129 is trying to acquire lock:
ffff8881088cb220 (&sb->s_type->i_mutex_key#24){+.+.}-{4:4}, at: inode_lock
include/linux/fs.h:1024 [inline]
ffff8881088cb220 (&sb->s_type->i_mutex_key#24){+.+.}-{4:4}, at:
configfs_depend_item_unlocked+0x153/0x420 fs/configfs/dir.c:1259

but task is already holding lock:
ffff888110c27110 (&opts->dep_lock){+.+.}-{4:4}, at:
usbg_make_tpg+0x147/0x590 drivers/usb/gadget/function/f_tcm.c:1678
...
Chain exists of:
  &sb->s_type->i_mutex_key#24 --> gadget_subsys.su_mutex -->
  &opts->dep_lock
======================================================

The circular dependency is formed by three concurrent operations: 1.
`usbg_make_tpg()` holds `opts->dep_lock` and calls
`configfs_depend_item_unlocked()`, which acquires the configfs
`inode_lock`. 2. `configfs_rmdir()` holds `inode_lock` and acquires
`gadget_subsys.su_mutex`. 3. `configfs_mkdir()` holds
`gadget_subsys.su_mutex` and calls `function_make()`, which eventually
acquires `opts->dep_lock` via `tcm_set_name()`.

To break this cycle, we must drop `opts->dep_lock` before calling
`configfs_depend_item_unlocked()`. However, doing so while still holding
`tpg_instances_lock` creates a new circular dependency:
`tpg_instances_lock` -> `inode_lock` -> `gadget_subsys.su_mutex` ->
`func_lock` -> `tpg_instances_lock`.

To fix this completely, we must drop both `opts->dep_lock` and
`tpg_instances_lock` before calling into the configfs or module subsystems
(`configfs_depend_item_unlocked()` or `try_module_get()`).

Dropping `tpg_instances_lock` introduces two new challenges: 1.
Concurrency: Another thread calling `usbg_make_tpg()` might find the same
`tpg_instances[i]` and try to use it concurrently. 2. Use-after-free: If
the user concurrently removes the function directory, `tcm_free_inst()`
could be called, freeing the `opts` structure while we are still using it.

To solve these challenges safely, we introduce a boolean `in_use` flag in
`struct tpg_instance` to mark an instance as "currently being configured".
This prevents other threads from picking the same instance while we have
dropped `tpg_instances_lock`. We also take a temporary reference to the
configfs item (`config_item_get_unless_zero()`) while holding
`tpg_instances_lock`. This guarantees that the `opts` structure cannot be
freed by `tcm_free_inst()` while we are doing our setup outside the lock.
The allocation of `tpg` is moved to the beginning of the function to
simplify the error handling path. Finally, we apply the same
reference-taking strategy in `usbg_drop_tpg()` to drop `tpg_instances_lock`
before calling `configfs_undepend_item_unlocked()`.

Fixes: 4bb8548df632 ("usb: gadget: f_tcm: add configfs support")
Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: [email protected]
Closes: https://syzkaller.appspot.com/bug?extid=c75a7db5ca6ae96ed2d4
Link: https://syzkaller.appspot.com/ai_job?id=3f265cbf-27c7-4edc-a58e-cab50809748f
To: "Greg Kroah-Hartman" <[email protected]>
To: <[email protected]>
To: "Andrzej Pietrasiewicz" <[email protected]>
Cc: "Jiasheng Jiang" <[email protected]>
Cc: <[email protected]>
Cc: "Cen Zhang" <[email protected]>

---
diff --git a/drivers/usb/gadget/function/f_tcm.c b/drivers/usb/gadget/function/f_tcm.c
index b3fa5a17f..6e195834a 100644
--- a/drivers/usb/gadget/function/f_tcm.c
+++ b/drivers/usb/gadget/function/f_tcm.c
@@ -31,6 +31,7 @@
 struct tpg_instance {
 	struct usb_function_instance	*func_inst;
 	struct usbg_tpg			*tpg;
+	bool				in_use;
 };
 
 static struct tpg_instance tpg_instances[TPG_INSTANCES];
@@ -1665,72 +1666,89 @@ static struct se_portal_group *usbg_make_tpg(struct se_wwn *wwn,
 		return ERR_PTR(-EINVAL);
 	if (kstrtou16(name + 5, 0, &tpgt))
 		return ERR_PTR(-EINVAL);
+
+	tpg = kzalloc_obj(struct usbg_tpg);
+	if (!tpg)
+		return ERR_PTR(-ENOMEM);
+	mutex_init(&tpg->tpg_mutex);
+	atomic_set(&tpg->tpg_port_count, 0);
+	tpg->workqueue = alloc_workqueue("tcm_usb_gadget",
+					 WQ_UNBOUND, WQ_UNBOUND_MAX_ACTIVE);
+	if (!tpg->workqueue) {
+		kfree(tpg);
+		return ERR_PTR(-ENOMEM);
+	}
+
+	tpg->tport = tport;
+	tpg->tport_tpgt = tpgt;
+
 	ret = -ENODEV;
 	mutex_lock(&tpg_instances_lock);
-	for (i = 0; i < TPG_INSTANCES; ++i)
-		if (tpg_instances[i].func_inst && !tpg_instances[i].tpg)
-			break;
-	if (i == TPG_INSTANCES)
-		goto unlock_inst;
+	for (i = 0; i < TPG_INSTANCES; ++i) {
+		if (tpg_instances[i].func_inst && !tpg_instances[i].tpg &&
+		    !tpg_instances[i].in_use) {
+			opts = container_of(tpg_instances[i].func_inst,
+					    struct f_tcm_opts, func_inst);
+			if (config_item_get_unless_zero(&opts->func_inst.group.cg_item))
+				break;
+		}
+	}
+	if (i == TPG_INSTANCES) {
+		mutex_unlock(&tpg_instances_lock);
+		goto free_workqueue;
+	}
+
+	tpg_instances[i].in_use = true;
+	mutex_unlock(&tpg_instances_lock);
 
-	opts = container_of(tpg_instances[i].func_inst, struct f_tcm_opts,
-		func_inst);
 	mutex_lock(&opts->dep_lock);
-	if (!opts->ready)
-		goto unlock_dep;
+	if (!opts->ready) {
+		mutex_unlock(&opts->dep_lock);
+		goto clear_in_use;
+	}
+	mutex_unlock(&opts->dep_lock);
 
 	if (opts->has_dep) {
 		if (!try_module_get(opts->dependent))
-			goto unlock_dep;
+			goto clear_in_use;
 	} else {
 		ret = configfs_depend_item_unlocked(
 			wwn->wwn_group.cg_subsys,
 			&opts->func_inst.group.cg_item);
 		if (ret)
-			goto unlock_dep;
+			goto clear_in_use;
 	}
 
-	tpg = kzalloc_obj(struct usbg_tpg);
-	ret = -ENOMEM;
-	if (!tpg)
-		goto unref_dep;
-	mutex_init(&tpg->tpg_mutex);
-	atomic_set(&tpg->tpg_port_count, 0);
-	tpg->workqueue = alloc_workqueue("tcm_usb_gadget",
-					 WQ_UNBOUND, WQ_UNBOUND_MAX_ACTIVE);
-	if (!tpg->workqueue)
-		goto free_tpg;
-
-	tpg->tport = tport;
-	tpg->tport_tpgt = tpgt;
-
 	/*
 	 * SPC doesn't assign a protocol identifier for USB-SCSI, so we
 	 * pretend to be SAS..
 	 */
 	ret = core_tpg_register(wwn, &tpg->se_tpg, SCSI_PROTOCOL_SAS);
 	if (ret < 0)
-		goto free_workqueue;
+		goto unref_dep;
 
+	mutex_lock(&tpg_instances_lock);
 	tpg_instances[i].tpg = tpg;
 	tpg->fi = tpg_instances[i].func_inst;
-	mutex_unlock(&opts->dep_lock);
+	tpg_instances[i].in_use = false;
 	mutex_unlock(&tpg_instances_lock);
+
+	config_item_put(&opts->func_inst.group.cg_item);
 	return &tpg->se_tpg;
 
-free_workqueue:
-	destroy_workqueue(tpg->workqueue);
-free_tpg:
-	kfree(tpg);
 unref_dep:
 	if (opts->has_dep)
 		module_put(opts->dependent);
 	else
 		configfs_undepend_item_unlocked(&opts->func_inst.group.cg_item);
-unlock_dep:
-	mutex_unlock(&opts->dep_lock);
-unlock_inst:
+clear_in_use:
+	mutex_lock(&tpg_instances_lock);
+	tpg_instances[i].in_use = false;
 	mutex_unlock(&tpg_instances_lock);
+	config_item_put(&opts->func_inst.group.cg_item);
+free_workqueue:
+	destroy_workqueue(tpg->workqueue);
+	kfree(tpg);
 
 	return ERR_PTR(ret);
 }
@@ -1755,16 +1773,21 @@ static void usbg_drop_tpg(struct se_portal_group *se_tpg)
 	if (i < TPG_INSTANCES) {
 		tpg_instances[i].tpg = NULL;
 		opts = container_of(tpg_instances[i].func_inst,
-			struct f_tcm_opts, func_inst);
-		mutex_lock(&opts->dep_lock);
+				    struct f_tcm_opts, func_inst);
+		config_item_get(&opts->func_inst.group.cg_item);
+	} else {
+		opts = NULL;
+	}
+	mutex_unlock(&tpg_instances_lock);
+
+	if (opts) {
 		if (opts->has_dep)
 			module_put(opts->dependent);
 		else
 			configfs_undepend_item_unlocked(
 				&opts->func_inst.group.cg_item);
-		mutex_unlock(&opts->dep_lock);
+		config_item_put(&opts->func_inst.group.cg_item);
 	}
-	mutex_unlock(&tpg_instances_lock);
 
 	kfree(tpg);
 }


base-commit: f5098b6bae761e346ebcd9da7f95622c04733cff
-- 
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).

See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
You can comment on the patch as usual, syzbot will try to address
the comments and send a new version of the patch if necessary.
syzbot engineers can be reached at [email protected].