* nvmet: pre-auth arbitrary kernel-memory read in Discovery Get-Log-Page (buffer + offset, unchecked attacker u64 lpo)
@ 2026-06-02 3:24 Jeremy Erazo
2026-06-02 8:34 ` Keith Busch
0 siblings, 1 reply; 2+ messages in thread
From: Jeremy Erazo @ 2026-06-02 3:24 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 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
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: nvmet: pre-auth arbitrary kernel-memory read in Discovery Get-Log-Page (buffer + offset, unchecked attacker u64 lpo)
2026-06-02 3:24 nvmet: pre-auth arbitrary kernel-memory read in Discovery Get-Log-Page (buffer + offset, unchecked attacker u64 lpo) Jeremy Erazo
@ 2026-06-02 8:34 ` Keith Busch
0 siblings, 0 replies; 2+ messages in thread
From: Keith Busch @ 2026-06-02 8:34 UTC (permalink / raw)
To: Jeremy Erazo
Cc: security, Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni,
Hannes Reinecke, Jens Axboe, linux-nvme, stable
On Mon, Jun 01, 2026 at 08:24:19PM -0700, Jeremy Erazo wrote:
> 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.
Duplicate report:
https://lore.kernel.org/linux-nvme/39YwPS5jntghiVQLt9ikZnmMc7O2g1AY3OVDcxdZjaK53FZHyzQNmyaS5eYBTS93g0Wc-S-UDC0auDRcGgC4iMR5RgXLEBPvqHfFZfbaeoU=@proton.me/
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-06-02 8:35 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-02 3:24 nvmet: pre-auth arbitrary kernel-memory read in Discovery Get-Log-Page (buffer + offset, unchecked attacker u64 lpo) Jeremy Erazo
2026-06-02 8:34 ` Keith Busch
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox