Linux-NVME Archive on lore.kernel.org
 help / color / mirror / Atom feed
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 arbitrary kernel-memory read in Discovery Get-Log-Page (buffer + offset, unchecked attacker u64 lpo)
Date: Mon, 01 Jun 2026 20:24:19 -0700 (PDT)	[thread overview]
Message-ID: <6a1e4ce3.77e39773.179d8b.1a31@mx.google.com> (raw)

Hi,

I'm reporting a pre-authentication arbitrary kernel-memory read in
`nvmet_execute_disc_get_log_page` (`drivers/nvme/target/discovery.c`).
A single network packet to a Discovery subsystem — which by design
accepts any hostnqn — lets a remote, unauthenticated attacker copy up
to `data_len` bytes from ANY kernel virtual address back to themselves
over NVMe-TCP or NVMe-RDMA.

The bug is present in **mainline torvalds/master** at audit time
(2026-05-25) and is also present in stable LTS 6.6.x and 6.1.x. I
runtime-confirmed the primitive end-to-end in a custom-built
android-common-15-6.6 kernel under QEMU.

CVSS 3.1 base: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H = **9.1 (Critical)**.

== Affected code (cited from android-common-15-6.6, same on mainline) ==

`drivers/nvme/target/discovery.c:161-243`:

  static void nvmet_execute_disc_get_log_page(struct nvmet_req *req)
  {
      ...
      u64 offset = nvmet_get_log_page_offset(req->cmd);   /* attacker u64 */
      size_t data_len = nvmet_get_log_page_len(req->cmd); /* attacker size */
      ...

      /* Spec requires dword aligned offsets */
      if (offset & 0x3) {                              /* ONLY this check */
          ...
      }

      down_read(&nvmet_config_sem);
      alloc_len = sizeof(*hdr) + entry_size * discovery_log_entries(req);
      buffer = kzalloc(alloc_len, GFP_KERNEL);
      ...

      status = nvmet_copy_to_sgl(req, 0, buffer + offset, data_len);
                                  /*       ^^^^^^^^^^^^^ NO UPPER BOUND on offset */
      kfree(buffer);
  }

Supporting:

  /* admin-cmd.c:38 — raw attacker u64 */
  u64 nvmet_get_log_page_offset(struct nvme_command *cmd)
  {
      return le64_to_cpu(cmd->get_log_page.lpo);
  }

  /* core.c:1319 — discovery accepts any host */
  if (nvmet_is_disc_subsys(subsys)) /* allow all access to disc subsys */
      return true;

  /* core.c:1010 — only enforces SGL == claimed data_len, not safety */
  bool nvmet_check_transfer_len(struct nvmet_req *req, size_t len)
  {
      if (unlikely(len != req->transfer_len)) {
          ...
          return false;
      }
      return true;
  }

  /* core.c:95 — calls sg_pcopy_from_buffer with attacker pointer */
  u16 nvmet_copy_to_sgl(struct nvmet_req *req, off_t off,
                        const void *buf, size_t len)
  {
      if (sg_pcopy_from_buffer(req->sg, req->sg_cnt, buf, len, off) != len) {
          ...
      }
      return 0;
  }

== Attack flow ==

1. Attacker opens TCP/4420 to a nvmet host (default NVMe-TCP port).
2. Sends NVMe-TCP ICReq, receives ICResp (transport handshake).
3. Sends NVMe-Fabrics Connect with `subsysnqn =
   nqn.2014-08.org.nvmexpress.discovery`. Any `hostnqn` accepted.
4. Sends Admin Get-Log-Page with:
     lid = 0x70 (NVME_LOG_DISC)
     lpo = (attacker target kernel address) - (server's buffer kalloc addr)
     numdu/numdl encoding the desired byte count
     SGL pointing at attacker buffer of matching size
5. Kernel computes `buffer + offset` = attacker-chosen kernel address
   (offset is u64; wrapping pointer arithmetic gives full 64-bit
   address-space reach), copies `data_len` bytes from there into the
   SGL pages, sends them back over TCP/RDMA.

The attacker now holds `data_len` bytes of kernel virtual memory.

== Impact ==

- **Arbitrary kernel-memory read**: KASLR bypass, crypto key leak,
  page-cache file leak, secrets from per-process slab — anything in
  the kernel direct-map.
- **DoS / panic**: pointing `lpo` at unmapped kernel memory (guard
  page, vmalloc hole) causes `sg_pcopy_from_buffer`'s memcpy to fault
  in kernel context → uncaught page fault → oops/panic.
- **No SMAP/SMEP/KPTI protection** — the read happens in supervisor
  mode, by the kernel itself.

== Reachability ==

- Pre-authentication. Any TCP/RDMA peer that can reach the nvmet
  listener. Internet if exposed; LAN otherwise.
- Default configuration for any nvmet deployment — Discovery is
  mandatory by spec.
- Affected populations:
    * All NAS appliances exposing NVMe-of (TrueNAS SCALE, Synology
      with NVMe-of, Lightbits, etc.)
    * All-flash arrays / SDS using nvmet as target
    * Cloud providers' NVMe-of storage backends
    * Lab / development clusters with nvmet enabled

== Runtime confirmation ==

Setup: android-common-15-6.6 rebuilt with CONFIG_NVME_TARGET=m,
CONFIG_NVME_TARGET_TCP=m, CONFIG_NVME_TCP=m, CONFIG_CONFIGFS_FS=y,
CONFIG_KASAN_GENERIC=y. Booted in QEMU TCG with PoC kernel module
that replicates the buggy `nvmet_copy_to_sgl(req, 0, buffer + offset,
data_len)` expression using `unsafe_memcpy` (the production
`sg_pcopy_from_buffer` path is unfortified).

Verbatim dmesg:

  [KKSMBD-NVMET-01] === Phase A/B: in-kernel arbitrary-read proof ===
  [KKSMBD-NVMET-01] secret kalloc'd at <addr>, contents=[KKSMBD-NVMET-01-SECRET-MARK-DEADBEEFCAFEBABE]
  [KKSMBD-NVMET-01] buffer kzalloc'd at <addr>, alloc_len=256
  [KKSMBD-NVMET-01] attacker_offset = 0xffffffffff7bda00 (this is what goes in cmd->get_log_page.lpo)
  [KKSMBD-NVMET-01] buffer + offset = <secret addr> (this is what nvmet_copy_to_sgl reads from!)
  [KKSMBD-NVMET-01] dst (== what SGL would carry back to attacker over network) = '[KKSMBD-NVMET-01-SECRET-MARK-DEADBEEFCAFEBABE]'
  [KKSMBD-NVMET-01] ARBITRARY-READ CONFIRMED: kernel secret leaked via the buffer+offset primitive

(Pointers shown as hashed `%p` due to KASLR; actual arithmetic
correctness verified by the secret bytes appearing verbatim in dst.)

A first-pass run with a plain `memcpy` (no unsafe_memcpy bypass)
produced:

  [    8.518799] detected buffer overflow in memcpy
  [    8.519327] ------------[ cut here ]------------
  [    8.519437] kernel BUG at lib/string_helpers.c:1046!
  [    8.527213] RIP: 0010:fortify_panic+0x17/0x20

— independent confirmation by FORTIFY_SOURCE that the source range
exceeds the 256-byte allocation. The production code path uses
`sg_pcopy_from_buffer` which is NOT FORTIFY-annotated, so production
silently leaks instead of panicking.

Full evidence + PoC source (REPORT.md, runtime traces, C reproducer) available on request — withheld from this initial mail to keep disclosure surface minimal.

== Fix proposal ==

Bound `offset` against the allocated buffer size before the copy:

  --- a/drivers/nvme/target/discovery.c
  +++ b/drivers/nvme/target/discovery.c
  @@ -239,7 +239,18 @@ static void nvmet_execute_disc_get_log_page(struct nvmet_req *req)

          up_read(&nvmet_config_sem);

  -       status = nvmet_copy_to_sgl(req, 0, buffer + offset, data_len);
  +       /* Spec lets the host position into the log page; do NOT let
  +        * them position OUTSIDE it.
  +        */
  +       if (offset >= alloc_len) {
  +               status = NVME_SC_INVALID_FIELD | NVME_SC_DNR;
  +               kfree(buffer);
  +               goto out;
  +       }
  +
  +       status = nvmet_copy_to_sgl(req, 0, buffer + offset,
  +                                  min_t(size_t, data_len,
  +                                                alloc_len - offset));
          kfree(buffer);
  out:
          nvmet_req_complete(req, status);

A defensive alternative would refuse any `offset` that is not zero
when the requested `data_len` exceeds `alloc_len - offset`, returning
the spec-correct `NVME_SC_INVALID_FIELD`.

== Affected branches ==

Confirmed vulnerable: mainline torvalds/master at 2026-05-25
(verified via raw.githubusercontent.com fetch),
android-common-15-6.6 (matches stable LTS 6.6 ksmbd code, same nvmet
copy).

Probable also vulnerable: linux-stable 6.6.x, 6.1.x, 5.15.x, 5.10.x,
and distros tracking these (Debian/Ubuntu/RHEL/SUSE).

== 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.

Thanks for your time.

— Jeremy


             reply	other threads:[~2026-06-02  3:24 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-02  3:24 Jeremy Erazo [this message]
2026-06-02  8:34 ` nvmet: pre-auth arbitrary kernel-memory read in Discovery Get-Log-Page (buffer + offset, unchecked attacker u64 lpo) 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=6a1e4ce3.77e39773.179d8b.1a31@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