Linux-NVME Archive on lore.kernel.org
 help / color / mirror / Atom feed
* nvmet: pre-auth heap OOB read in DH-HMAC-CHAP authentication (data->hl unchecked in nvmet_auth_reply)
@ 2026-06-02  3:32 Jeremy Erazo
  2026-06-02  6:23 ` Greg KH
  2026-06-02  8:50 ` Keith Busch
  0 siblings, 2 replies; 5+ messages in thread
From: Jeremy Erazo @ 2026-06-02  3:32 UTC (permalink / raw)
  To: security
  Cc: Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
	Hannes Reinecke, Keith Busch, Jens Axboe, linux-nvme, stable

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


^ permalink raw reply	[flat|nested] 5+ messages in thread
* nvmet: pre-auth heap OOB read in DH-HMAC-CHAP authentication (data->hl unchecked in nvmet_auth_reply)
@ 2026-06-02  3:32 Jeremy Erazo
  0 siblings, 0 replies; 5+ messages in thread
From: Jeremy Erazo @ 2026-06-02  3:32 UTC (permalink / raw)
  To: security
  Cc: Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
	Hannes Reinecke, Keith Busch, Jens Axboe, linux-nvme, stable

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


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-06-02  8:57 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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
  -- strict thread matches above, loose matches on Subject: below --
2026-06-02  3:32 Jeremy Erazo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox