From 12740bec646dafe009a7d9a4d22d3495ef1ea5cd Mon Sep 17 00:00:00 2001 From: Andrea Parri Date: Thu, 22 Oct 2026 15:29:31 +0200 Subject: [PATCH] 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: aaf2e048af27 ("nvmet: add ZBD over ZNS backend support") Signed-off-by: Andrea Parri Signed-off-by: Keith Busch --- 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 291d782ce544..2e4e066ef6ba 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 78d82b6ff121..92f5c1435582 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); - 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);