[PATCH] nvme: keep the effects log per namespace path, not in the shared head
From: Nguyen Ngoc Thang
Date: Fri Oct 02 2026 - 12:31:13 EST
nvme_alloc_ns_head() points head->effects at an effects log owned by the
controller that created the head: an entry in that controller's ctrl->cels
xarray, or ctrl->effects for the NVM command set, which also lives there.
The head is shared by all controllers attached to the same namespace and
can outlive its creator. Once that controller is deleted, nvme_free_cels()
frees the log while the head still points at it.
Any later user then reads freed memory: nvme_query_zone_info() when another
controller scans or rescans a zoned namespace, as reported by syzbot, and
nvme_command_effects() for I/O passthrough on a surviving path.
BUG: KASAN: slab-use-after-free in nvme_query_zone_info+0x4fd/0x6f0
Read of size 4 at addr ffff88803745a5f4 by task kworker/u8:13/5985
nvme_query_zone_info+0x4fd/0x6f0 drivers/nvme/host/zns.c:47
nvme_update_ns_info_block+0x1b2e/0x2af0 drivers/nvme/host/core.c:2430
nvme_update_ns_info+0xc6/0xd90 drivers/nvme/host/core.c:2553
nvme_alloc_ns+0x1a9f/0x3e50 drivers/nvme/host/core.c:4293
nvme_scan_ns+0x79d/0x910 drivers/nvme/host/core.c:4483
Freed by task 5948:
kfree+0x22b/0x6d0 mm/slub.c:6801
nvme_free_cels drivers/nvme/host/core.c:5165 [inline]
nvme_free_ctrl+0x1e4/0x6b0 drivers/nvme/host/core.c:5183
device_release+0xd2/0x270 drivers/base/core.c:2640
put_device+0x1f/0x30 drivers/base/core.c:3884
nvme_sysfs_delete+0xc9/0xf0 drivers/nvme/host/sysfs.c:500
The Commands Supported and Effects log is a per-controller property, so
store the pointer in struct nvme_ns instead and look it up for every
namespace path in nvme_init_ns_head(). Each nvme_ns holds a reference on
its controller, so the log stays valid for as long as the path exists,
and commands are checked against the log of the controller that actually
executes them. The lookup stays under subsys->lock, which already
serialized nvme_get_effects_log() for head creation.
Reproduced with two nvme-loop controllers connected to one subsystem
backed by a zoned null_blk device: delete the first controller, then
rescan the second or connect a third.
Fixes: be93e87e7802 ("nvme: support for multiple Command Sets Supported and Effects log pages")
Cc: stable@xxxxxxxxxxxxxxx
Reported-by: syzbot+76c0f0ce8f1e846b4f84@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=76c0f0ce8f1e846b4f84
Signed-off-by: Nguyen Ngoc Thang <ngocthang2710.1999@xxxxxxxxx>
---
Tested in QEMU (x86_64, KASAN) on v7.3-rc5:
- Deterministic: two nvme-loop controllers on one subsystem backed by a
zoned null_blk, delete nvme0, rescan nvme1, connect a third. Before:
KASAN slab-use-after-free in nvme_query_zone_info() on both the rescan
and the new-controller path. After: clean.
- syzbot C reproducer, kasan_multi_shot: 175 reports before; 0 reports
in two runs after (~900 controllers created/deleted per run).
- Zone Append is still detected on every path (no "Forcing to
read-only mode"), and direct I/O to the zoned namespace works.
Deepanshu, Cc'ing you since you posted a test patch on this report.
Pinning the scanning controller in scan_work doesn't cover this case:
the freed log belongs to a different controller, the one that created
the shared head.
drivers/nvme/host/core.c | 21 +++++++++++++--------
drivers/nvme/host/nvme.h | 2 +-
drivers/nvme/host/zns.c | 2 +-
3 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
index beea23d04a70..e0fea859dfaa 100644
--- a/drivers/nvme/host/core.c
+++ b/drivers/nvme/host/core.c
@@ -1234,7 +1234,7 @@ u32 nvme_command_effects(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode)
u32 effects = 0;
if (ns) {
- effects = le32_to_cpu(ns->head->effects->iocs[opcode]);
+ effects = le32_to_cpu(ns->effects->iocs[opcode]);
if (effects & ~(NVME_CMD_EFFECTS_CSUPP | NVME_CMD_EFFECTS_LBCC))
dev_warn_once(ctrl->device,
"IO command:%02x has unusual effects:%08x\n",
@@ -4033,13 +4033,6 @@ static struct nvme_ns_head *nvme_alloc_ns_head(struct nvme_ns *ns,
kref_init(&head->ref);
ns->head = head;
- if (head->ids.csi) {
- ret = nvme_get_effects_log(ctrl, head->ids.csi, &head->effects);
- if (ret)
- goto out_cleanup_srcu;
- } else
- head->effects = ctrl->effects;
-
if (ctrl->ctratt & NVME_CTRL_ATTR_FDPS) {
ret = nvme_query_fdp_info(ns, info);
if (ret < 0)
@@ -4141,6 +4134,18 @@ static int nvme_init_ns_head(struct nvme_ns *ns, struct nvme_ns_info *info)
}
mutex_lock(&ctrl->subsys->lock);
+ /*
+ * The effects log belongs to this controller, not to the shared head,
+ * which may outlive the controller that created it.
+ */
+ if (info->ids.csi) {
+ ret = nvme_get_effects_log(ctrl, info->ids.csi, &ns->effects);
+ if (ret)
+ goto out_unlock;
+ } else {
+ ns->effects = ctrl->effects;
+ }
+
head = nvme_find_ns_head(ctrl, info->nsid);
if (!head) {
ret = nvme_subsys_check_duplicate_ids(ctrl->subsys, &info->ids);
diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h
index 2cff9fcbf740..7d6ced8ed78b 100644
--- a/drivers/nvme/host/nvme.h
+++ b/drivers/nvme/host/nvme.h
@@ -554,7 +554,6 @@ struct nvme_ns_head {
bool shared;
bool rotational;
bool passthru_err_log_enabled;
- struct nvme_effects_log *effects;
u64 nuse;
unsigned ns_id;
int instance;
@@ -620,6 +619,7 @@ struct nvme_ns {
struct list_head siblings;
struct kref kref;
struct nvme_ns_head *head;
+ struct nvme_effects_log *effects;
unsigned long flags;
#define NVME_NS_REMOVING 0
diff --git a/drivers/nvme/host/zns.c b/drivers/nvme/host/zns.c
index e31ec6f4f94f..ef75448036f7 100644
--- a/drivers/nvme/host/zns.c
+++ b/drivers/nvme/host/zns.c
@@ -38,7 +38,7 @@ static int nvme_set_max_append(struct nvme_ctrl *ctrl)
int nvme_query_zone_info(struct nvme_ns *ns, unsigned lbaf,
struct nvme_zone_info *zi)
{
- struct nvme_effects_log *log = ns->head->effects;
+ struct nvme_effects_log *log = ns->effects;
struct nvme_command c = { };
struct nvme_id_ns_zns *id;
int status;
--
2.43.0