[PATCH v2 2/4] nvmet: zns: reject a NUMD whose byte count wraps
From: Andrea Parri
Date: Thu Oct 22 2026 - 09:38:05 EST
Zone Management Receive computes its buffer size as (numd + 1) << 2 in
u32, in nvmet_bdev_validate_zone_mgmt_recv(),
nvmet_bdev_zone_zmgmt_recv_work() and
nvmet_bdev_execute_zone_mgmt_recv(). NUMD is a 32-bit dword count, so
any NUMD from 0x3fffffff up wraps.
The data length check in nvmet_bdev_execute_zone_mgmt_recv() sees the
same wrapped value, so such a command can pass it. A data length that
does not match the wrapped size is already rejected by that check, with
Data SGL Length Invalid.
Every site agrees on the wrapped size, so the target zeroes and fills
only that many bytes and nothing leaks, but it answers a command whose
NUMD asks for 4 GiB or more.
Reject a NUMD from U32_MAX / 4 up with Invalid Field at the start of
nvmet_bdev_execute_zone_mgmt_recv(), before this handler or its work
item computes the byte count. A size_t computation would still wrap on
32-bit kernels, where nvmet_check_transfer_len() and req->transfer_len
are 32 bits wide.
nvmet_io_cmd_transfer_len() in core.c, which the pci-epf transport uses
to size its buffer, computes the same expression before this handler
runs. Return 0 there for such a NUMD, so pci-epf allocates no buffer and
the handler then rejects the command.
Tested over the loop, tcp, rdma and fc transports with a zoned
null_blk namespace: a wrapping NUMD, sent with a data length that
matches the wrapped size, was accepted before this patch and now fails
with Invalid Field, while ordinary NUMD values still succeed. The core.c
change only affects pci-epf and is build-tested only.
Fixes: aaf2e048af27 ("nvmet: add ZBD over ZNS backend support")
Signed-off-by: Andrea Parri <parri.andrea@xxxxxxxxx>
---
v2: check NUMD at the start of nvmet_bdev_execute_zone_mgmt_recv() and in
nvmet_io_cmd_transfer_len(), before any (numd + 1) << 2 is computed,
instead of in nvmet_bdev_validate_zone_mgmt_recv(); this also drops the
over-long comment line (Christoph).
drivers/nvme/target/core.c | 3 +++
drivers/nvme/target/zns.c | 12 ++++++++++--
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c
index 291d782ce5442..2e4e066ef6bac 100644
--- a/drivers/nvme/target/core.c
+++ b/drivers/nvme/target/core.c
@@ -1093,6 +1093,9 @@ static u32 nvmet_io_cmd_transfer_len(struct nvmet_req *req)
case nvme_cmd_dsm:
return nvmet_dsm_len(req);
case nvme_cmd_zone_mgmt_recv:
+ /* The handler rejects a NUMD whose byte count would wrap. */
+ if (le32_to_cpu(req->cmd->zmr.numd) >= U32_MAX / 4)
+ return 0;
return (le32_to_cpu(req->cmd->zmr.numd) + 1) << 2;
default:
return 0;
diff --git a/drivers/nvme/target/zns.c b/drivers/nvme/target/zns.c
index 78d82b6ff1214..92f5c1435582e 100644
--- a/drivers/nvme/target/zns.c
+++ b/drivers/nvme/target/zns.c
@@ -322,9 +322,17 @@ static void nvmet_bdev_zone_zmgmt_recv_work(struct work_struct *w)
void nvmet_bdev_execute_zone_mgmt_recv(struct nvmet_req *req)
{
- u32 out_bufsize = (le32_to_cpu(req->cmd->zmr.numd) + 1) << 2;
+ u32 numd = le32_to_cpu(req->cmd->zmr.numd);
+
+ /* From U32_MAX / 4 up, (numd + 1) << 2 wraps here and in the work. */
+ if (numd >= U32_MAX / 4) {
+ req->error_loc = offsetof(struct nvme_zone_mgmt_recv_cmd, numd);
+ nvmet_req_complete(req,
+ NVME_SC_INVALID_FIELD | NVME_STATUS_DNR);
+ return;
+ }
- if (!nvmet_check_transfer_len(req, out_bufsize))
+ if (!nvmet_check_transfer_len(req, (numd + 1) << 2))
return;
INIT_WORK(&req->z.zmgmt_work, nvmet_bdev_zone_zmgmt_recv_work);
--
2.53.0