nvmet: zns: reject a NUMD whose byte count wraps

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: aaf2e048af ("nvmet: add ZBD over ZNS backend support")
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
Signed-off-by: Keith Busch <kbusch@kernel.org>
This commit is contained in:
Andrea Parri 2026-10-22 15:29:31 +02:00 committed by Keith Busch
parent 94f93c61b1
commit 12740bec64
2 changed files with 13 additions and 2 deletions

View File

@ -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;

View File

@ -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);
if (!nvmet_check_transfer_len(req, out_bufsize))
/* 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, (numd + 1) << 2))
return;
INIT_WORK(&req->z.zmgmt_work, nvmet_bdev_zone_zmgmt_recv_work);