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

"syzbot" <[email protected]> Mon, 3 Aug 2026 13:33:40 +0000 (UTC)
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
A circular locking dependency was detected involving `inode_lock`,
`gadget_subsys.su_mutex`, `opts->dep_lock`, and `tpg_instances_lock`.

The cycle is formed by the following chains:

1. `inode_lock` -> `gadget_subsys.su_mutex`: When creating a symlink in
configfs (e.g., for UVC extensions), the VFS layer calls configfs_symlink()
with the inode_lock of the parent directory held. configfs_symlink() then
calls the allow_link callback, which acquires gadget_subsys.su_mutex.

2. `gadget_subsys.su_mutex` -> `opts->dep_lock` / `tpg_instances_lock`:
When creating a new TCM USB gadget function directory, configfs_mkdir()
acquires gadget_subsys.su_mutex and calls function_make(). This calls
tcm_set_name(), which acquires opts->dep_lock. It also acquires
tpg_instances_lock during instance allocation.

3. `opts->dep_lock` / `tpg_instances_lock` -> `inode_lock`: When creating a
new Target Portal Group (TPG) directory for the TCM USB gadget,
usbg_make_tpg() is called. It acquires tpg_instances_lock and
opts->dep_lock. While holding these locks, it calls
configfs_depend_item_unlocked(), which acquires the inode_lock of the
configfs root directory.

This leads to the following lockdep splat:

======================================================
WARNING: possible circular locking dependency detected
------------------------------------------------------
task is trying to acquire lock:
(gadget_subsys.su_mutex){+.+.}-{4:4}, at:
uvcg_extension_allow_link+0x70/0x1f8

but task is already holding lock:
(&sb->s_type->i_mutex_key#23){+.+.}-{4:4}, at: inode_lock
include/linux/fs.h:1024 [inline]

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

-> #2 (&sb->s_type->i_mutex_key#23){+.+.}-{4:4}:
       inode_lock include/linux/fs.h:1024 [inline]
       configfs_depend_item_unlocked+0x154/0x344 fs/configfs/dir.c:1259
       usbg_make_tpg+0x230/0x4b4 drivers/usb/gadget/function/f_tcm.c:1686
       target_fabric_make_tpg+0x98/0x518
       drivers/target/target_core_fabric_configfs.c:939
       configfs_mkdir+0x374/0x7d4 fs/configfs/dir.c:1360

-> #1 (&opts->dep_lock){+.+.}-{4:4}:
       __mutex_lock+0x164/0xf14 kernel/locking/mutex.c:821
       tcm_set_name+0x34/0xd0 drivers/usb/gadget/function/f_tcm.c:2669
       function_make+0x18c/0x300 drivers/usb/gadget/configfs.c:636
       configfs_mkdir+0x374/0x7d4 fs/configfs/dir.c:1360

-> #0 (gadget_subsys.su_mutex){+.+.}-{4:4}:
       __mutex_lock+0x164/0xf14 kernel/locking/mutex.c:821
       uvcg_extension_allow_link+0x70/0x1f8
       drivers/usb/gadget/function/uvc_configfs.c:1195
       configfs_symlink+0x478/0xde0 fs/configfs/symlink.c:196

To break the cycle, we must drop both opts->dep_lock and tpg_instances_lock
before calling configfs_depend_item_unlocked() in usbg_make_tpg() and
configfs_undepend_item_unlocked() in usbg_drop_tpg().

Dropping tpg_instances_lock introduces two potential race conditions that
are handled as follows:

1. Slot Reservation Race: usbg_make_tpg() finds a free slot by checking
!tpg_instances[i].tpg. To prevent a concurrent usbg_make_tpg() from picking
the same slot after the lock is dropped, we allocate the tpg structure and
assign it to tpg_instances[i].tpg before dropping the lock.

2. Use-After-Free Race: A concurrent removal of the function directory
could trigger tcm_free_inst(), freeing the opts structure. To prevent this,
we take a temporary reference on the configfs item using config_item_get()
before dropping the lock, and release it with config_item_put() afterwards.

Similarly, in usbg_drop_tpg(), we delay setting tpg_instances[i].tpg = NULL
until after we have called configfs_undepend_item_unlocked() and
re-acquired the lock, keeping the slot reserved.

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=b3410eb576b568216ae5
Link: https://syzkaller.appspot.com/ai_job?id=deadd783-1210-4cb4-976a-2e037b0ca322
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..e5fbb8ca0 100644
--- a/drivers/usb/gadget/function/f_tcm.c
+++ b/drivers/usb/gadget/function/f_tcm.c
@@ -1660,6 +1660,7 @@ static struct se_portal_group *usbg_make_tpg(struct se_wwn *wwn,
 	int ret;
 	struct f_tcm_opts *opts;
 	unsigned i;
+	bool has_dep;
 
 	if (strstr(name, "tpgt_") != name)
 		return ERR_PTR(-EINVAL);
@@ -1676,19 +1677,19 @@ static struct se_portal_group *usbg_make_tpg(struct se_wwn *wwn,
 	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 unlock_inst;
+	}
 
-	if (opts->has_dep) {
-		if (!try_module_get(opts->dependent))
-			goto unlock_dep;
-	} else {
-		ret = configfs_depend_item_unlocked(
-			wwn->wwn_group.cg_subsys,
-			&opts->func_inst.group.cg_item);
-		if (ret)
-			goto unlock_dep;
+	has_dep = opts->has_dep;
+	if (has_dep) {
+		if (!try_module_get(opts->dependent)) {
+			mutex_unlock(&opts->dep_lock);
+			goto unlock_inst;
+		}
 	}
+	mutex_unlock(&opts->dep_lock);
 
 	tpg = kzalloc_obj(struct usbg_tpg);
 	ret = -ENOMEM;
@@ -1704,31 +1705,44 @@ static struct se_portal_group *usbg_make_tpg(struct se_wwn *wwn,
 	tpg->tport = tport;
 	tpg->tport_tpgt = tpgt;
 
+	tpg_instances[i].tpg = tpg;
+	tpg->fi = tpg_instances[i].func_inst;
+
+	config_item_get(&opts->func_inst.group.cg_item);
+	mutex_unlock(&tpg_instances_lock);
+
+	if (!has_dep) {
+		ret = configfs_depend_item_unlocked(
+			wwn->wwn_group.cg_subsys,
+			&opts->func_inst.group.cg_item);
+		if (ret)
+			goto put_item;
+	}
+
 	/*
 	 * 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 undepend_item;
 
-	tpg_instances[i].tpg = tpg;
-	tpg->fi = tpg_instances[i].func_inst;
-	mutex_unlock(&opts->dep_lock);
-	mutex_unlock(&tpg_instances_lock);
+	config_item_put(&opts->func_inst.group.cg_item);
 	return &tpg->se_tpg;
 
-free_workqueue:
+undepend_item:
+	if (!has_dep)
+		configfs_undepend_item_unlocked(&opts->func_inst.group.cg_item);
+put_item:
+	config_item_put(&opts->func_inst.group.cg_item);
 	destroy_workqueue(tpg->workqueue);
+	mutex_lock(&tpg_instances_lock);
+	tpg_instances[i].tpg = NULL;
 free_tpg:
 	kfree(tpg);
 unref_dep:
-	if (opts->has_dep)
+	if (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:
 	mutex_unlock(&tpg_instances_lock);
 
@@ -1743,6 +1757,7 @@ static void usbg_drop_tpg(struct se_portal_group *se_tpg)
 				struct usbg_tpg, se_tpg);
 	unsigned i;
 	struct f_tcm_opts *opts;
+	bool has_dep;
 
 	tcm_usbg_drop_nexus(tpg);
 	core_tpg_deregister(se_tpg);
@@ -1753,16 +1768,28 @@ static void usbg_drop_tpg(struct se_portal_group *se_tpg)
 		if (tpg_instances[i].tpg == tpg)
 			break;
 	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);
-		if (opts->has_dep)
+		has_dep = opts->has_dep;
+		mutex_unlock(&opts->dep_lock);
+
+		config_item_get(&opts->func_inst.group.cg_item);
+		mutex_unlock(&tpg_instances_lock);
+
+		if (has_dep)
 			module_put(opts->dependent);
 		else
 			configfs_undepend_item_unlocked(
 				&opts->func_inst.group.cg_item);
-		mutex_unlock(&opts->dep_lock);
+
+		mutex_lock(&tpg_instances_lock);
+		tpg_instances[i].tpg = NULL;
+		mutex_unlock(&tpg_instances_lock);
+
+		config_item_put(&opts->func_inst.group.cg_item);
+		kfree(tpg);
+		return;
 	}
 	mutex_unlock(&tpg_instances_lock);
 


base-commit: 075b74841bd0065a3bda3440873c747938e69b68
-- 
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].