[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].