From: Jeremy Erazo <mendozayt13@gmail.com>
To: security@kernel.org
Cc: Christoph Hellwig <hch@infradead.org>,
Sagi Grimberg <sagi@grimberg.me>,
Chaitanya Kulkarni <kch@nvidia.com>,
Hannes Reinecke <hare@suse.de>, Keith Busch <kbusch@kernel.org>,
Jens Axboe <axboe@kernel.dk>,
linux-nvme@lists.infradead.org, stable@vger.kernel.org
Subject: nvmet: pre-auth heap OOB read in DH-HMAC-CHAP authentication (data->hl unchecked in nvmet_auth_reply)
Date: Mon, 01 Jun 2026 20:32:45 -0700 (PDT) [thread overview]
Message-ID: <6a1e4edd.541e6ed7.68248.dfe4@mx.google.com> (raw)
Hi,
I'm reporting an out-of-bounds read in `nvmet_auth_reply()`
(`drivers/nvme/target/fabrics-cmd-auth.c`), reachable from an
unauthenticated remote attacker against any nvmet host that has
DH-HMAC-CHAP authentication enabled on a configured subsystem.
The bug is present in **mainline torvalds/master** at audit time
(2026-06-02, verified via raw.githubusercontent.com fetch). It is
also present in stable LTS 6.6.x and other branches that ship the
NVMe-oF DH-HMAC-CHAP target (the function was added with the
DH-HMAC-CHAP target feature in 6.0).
CVSS 3.1 base: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N = **7.5 (High)**.
This is filed shortly after my earlier report of an arbitrary
kernel-memory read in `nvmet_execute_disc_get_log_page` (sent today
to this list). Both are in `drivers/nvme/target/` and both are
attacker-controlled-length-meets-pointer-arithmetic class — but they
are independent bugs with independent fixes.
== Affected code (cited from torvalds/master, 2026-06-02) ==
`drivers/nvme/target/fabrics-cmd-auth.c::nvmet_execute_auth_send()`
(the AUTH_Send PDU dispatcher) does:
tl = le32_to_cpu(req->cmd->auth_send.tl); /* attacker u32 */
if (!tl) { ... goto done; }
if (!nvmet_check_transfer_len(req, tl)) return;
d = kmalloc(tl, GFP_KERNEL);
if (!d) { ... }
status = nvmet_copy_from_sgl(req, 0, d, tl);
/* ... dispatch by data->auth_id ... */
if (data->auth_id == NVME_AUTH_DHCHAP_MESSAGE_REPLY)
dhchap_status = nvmet_auth_reply(req, d);
The ONLY validation of `tl` is "non-zero" and that the SGL transfer
length matches the command's declared length. There is no check that
`tl` is large enough to contain the struct header *plus* the
payload that the message body advertises.
`nvmet_auth_reply()` then does:
struct nvmf_auth_dhchap_reply_data *data = d;
u16 dhvlen = le16_to_cpu(data->dhvlen);
u8 *response;
if (dhvlen) {
if (!ctrl->dh_tfm) ...
if (nvmet_auth_ctrl_sesskey(req, data->rval + 2 * data->hl, /* OOB#3 */
dhvlen) < 0) ...
}
response = kmalloc(data->hl, GFP_KERNEL);
...
if (nvmet_auth_host_hash(req, response, data->hl) < 0) ...
if (memcmp(data->rval, response, data->hl)) /* OOB#1 */
...
if (data->cvalid) {
req->sq->dhchap_c2 = kmemdup(data->rval + data->hl, data->hl, /* OOB#2 */
GFP_KERNEL);
...
}
Three call sites use `data->hl` (an attacker-controlled u8, range
0-255 from the wire) and `data->dhvlen` (an attacker-controlled
__le16, range 0-65535) as offsets into / lengths reading from
`data->rval` without any prior check that the PDU transfer length
contained the corresponding bytes.
Supporting struct (`include/linux/nvme.h:1712`):
struct nvmf_auth_dhchap_reply_data {
__u8 auth_type;
__u8 auth_id;
__le16 rsvd1;
__le16 t_id;
__u8 hl; /* attacker u8 */
__u8 rsvd2;
__u8 cvalid;
__u8 rsvd3;
__le16 dhvlen; /* attacker __le16 */
__le32 seqnum;
/* 'hl' bytes of response data */
__u8 rval[];
/* followed by 'hl' bytes of Challenge value */
/* followed by 'dhvlen' bytes of DH value */
};
`sizeof(struct nvmf_auth_dhchap_reply_data)` = 16 bytes (header only).
== Attack flow ==
1. Attacker opens TCP/4420 to a nvmet host that has DH-HMAC-CHAP
configured on at least one subsystem.
2. Sends NVMe-TCP ICReq, receives ICResp (transport handshake).
3. Sends NVMe-Fabrics Connect with `subsysnqn = <target subsys with
CHAP>`, any `hostnqn`. (Connect succeeds; the controller now
expects the host to drive the CHAP exchange.)
4. Sends AUTH_Send with `auth_id = MESSAGE_NEGOTIATE`, advertising
any supported hash (e.g., SHA-256).
5. AUTH_Receive — controller sends CHALLENGE.
6. Sends a malicious AUTH_Send with:
auth_type = NVME_AUTH_DHCHAP_MESSAGES (1)
auth_id = NVME_AUTH_DHCHAP_MESSAGE_REPLY (2)
tl = 16 (sizeof(struct nvmf_auth_dhchap_reply_data) only;
no rval[] payload)
data->hl = 0xff
data->cvalid = 1
data->dhvlen = 0
7. Kernel:
- kmalloc(16) returns a kmalloc-16 slab object (SLUB rounds up).
- copy_from_sgl writes the 16-byte attacker header into it.
- data = d; data->rval = d + 16 = past the allocation.
- memcmp(data->rval, response, 255) reads 255 bytes starting at
offset 16 of the kmalloc-16 slab object — directly into the
adjacent slab object.
- If data->cvalid is set, kmemdup(data->rval + 255, 255) reads
an additional 510 bytes past the allocation and copies 255 of
them into a new kernel allocation (which the attacker can
later exfil via further wire messages if the CHAP exchange
continues, e.g., via AUTH_Receive's payload).
== KASAN catch signature (expected) ==
Under KASAN this produces a slab-out-of-bounds READ report tied to
`memcmp` / `kmemdup` called from `nvmet_auth_reply`:
BUG: KASAN: slab-out-of-bounds in memcmp+0x... (or __asan_memcmp)
Read of size 255 at addr ffff... by task kworker/...
Call Trace:
memcmp
nvmet_auth_reply
nvmet_execute_auth_send
nvmet_tcp_io_work
...
I've prepared an in-kernel proof module
(`nvmet-auth-oob-proof.c`) that replicates the primitive against a
deliberately-undersized buffer to make the KASAN signature explicit;
it's a one-shot module that builds against any current 6.x kernel
tree. Available on request — withheld from this initial mail to keep
disclosure surface minimal.
A userspace network reproducer (Python NVMe-TCP client driving the
CHAP state machine through the malicious AUTH_Send) is in progress
and will follow.
== Fix proposal ==
Validate the PDU transfer length covers the struct header *plus* the
hl- and dhvlen-derived payload before any pointer arithmetic on
data->rval. The minimal fix sits at the entry to nvmet_auth_reply:
--- a/drivers/nvme/target/fabrics-cmd-auth.c
+++ b/drivers/nvme/target/fabrics-cmd-auth.c
@@ -112,6 +112,7 @@ static u8 nvmet_auth_reply(struct nvmet_req *req, void *d)
{
struct nvmet_ctrl *ctrl = req->sq->ctrl;
struct nvmf_auth_dhchap_reply_data *data = d;
+ u32 tl = le32_to_cpu(req->cmd->auth_send.tl);
u16 dhvlen = le16_to_cpu(data->dhvlen);
u8 *response;
@@ -119,6 +120,16 @@ static u8 nvmet_auth_reply(struct nvmet_req *req, void *d)
__func__, ctrl->cntlid, req->sq->qid,
data->hl, data->cvalid, dhvlen);
+ /* Confirm the transferred length actually contains the
+ * rval payload the message body advertises. The host
+ * response is hl bytes; with cvalid set, hl more bytes
+ * of challenge follow; with dhvlen set, dhvlen more
+ * bytes of DH value follow.
+ */
+ if (tl < sizeof(*data) + data->hl +
+ (data->cvalid ? data->hl : 0) + dhvlen)
+ return NVME_AUTH_DHCHAP_FAILURE_INCORRECT_PAYLOAD;
+
if (dhvlen) {
if (!ctrl->dh_tfm)
return NVME_AUTH_DHCHAP_FAILURE_INCORRECT_PAYLOAD;
The same shape applies to `nvmet_auth_negotiate` and any other
auth-state handler that reads variable-length fields from the
attacker buffer. A defence-in-depth alternative is to do this length
validation once inside `nvmet_execute_auth_send` before dispatching,
since the handler-specific math (hl, dhvlen, etc.) varies per
message type.
== Affected branches ==
Confirmed vulnerable: mainline torvalds/master at 2026-06-02
(verified via raw.githubusercontent.com fetch).
Probable also vulnerable: linux-stable 6.6.x, 6.1.x (where the
DH-HMAC-CHAP target was backported), and distros tracking those
(Debian/Ubuntu/RHEL/SUSE).
NOT vulnerable: any kernel without `CONFIG_NVME_TARGET_AUTH=y` (the
auth subsystem isn't compiled in).
== Threat model ==
This is reachable pre-final-authentication: the attacker has
completed Fabrics Connect (which establishes the controller binding)
but has not yet completed the CHAP handshake. CHAP is exactly what
this code is supposed to enforce; the bug is in the CHAP enforcement
itself. The kernel cannot rely on CHAP being effective when the CHAP
handler can be tricked into reading past its input buffer before any
secret is verified.
Network reachability is "any host that can open TCP/4420 to the
nvmet target". In production NVMe-oF deployments this is the same
exposure surface that DH-HMAC-CHAP exists to protect — i.e., the
mitigation is the bug.
== Researcher / Credit ==
Jeremy Erazo (trexnegr0)
mendozayt13@gmail.com
Signed-off-by: Jeremy Erazo <mendozayt13@gmail.com>
== Disclosure preferences ==
I'm happy with any reasonable embargo length (14-30 days). I have
not shared this finding with any third party. Please coordinate CVE
assignment with the kernel.org CNA.
This is the second nvmet finding I'm reporting today; both are
independent bugs but they neighbour each other in the same subsystem
and one combined backport batch on the stable side might be the
cleanest disposition. Happy to coordinate that with whichever route
your team prefers.
Thanks for your time.
— Jeremy
next reply other threads:[~2026-06-02 3:32 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-02 3:32 Jeremy Erazo [this message]
-- strict thread matches above, loose matches on Subject: below --
2026-06-02 3:32 nvmet: pre-auth heap OOB read in DH-HMAC-CHAP authentication (data->hl unchecked in nvmet_auth_reply) Jeremy Erazo
2026-06-02 6:23 ` Greg KH
2026-06-02 8:50 ` Keith Busch
2026-06-02 8:56 ` Keith Busch
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=6a1e4edd.541e6ed7.68248.dfe4@mx.google.com \
--to=mendozayt13@gmail.com \
--cc=axboe@kernel.dk \
--cc=hare@suse.de \
--cc=hch@infradead.org \
--cc=kbusch@kernel.org \
--cc=kch@nvidia.com \
--cc=linux-nvme@lists.infradead.org \
--cc=sagi@grimberg.me \
--cc=security@kernel.org \
--cc=stable@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox