Skip to content

io: bounds-check the LBA range and validate PRPs in __do_perform_io#75

Open
jongukc wants to merge 1 commit into
snu-csl:mainfrom
jongukc:fix/io-lba-range-and-prp-validation
Open

io: bounds-check the LBA range and validate PRPs in __do_perform_io#75
jongukc wants to merge 1 commit into
snu-csl:mainfrom
jongukc:fix/io-lba-range-and-prp-validation

Conversation

@jongukc

@jongukc jongukc commented Jun 18, 2026

Copy link
Copy Markdown

Summary

__do_perform_io() builds the backing-store address from host command fields without checking them.

It takes the namespace index from cmd->nsid, the offset from cmd->slba, and the length from cmd->length.
It then runs a memcpy to or from nvmev_vdev->ns[nsid].mapped + offset.

A normal block read/write is bounded by the disk size, so this is safe in normal use.
But a passthrough I/O command lets user space set slba, nblocks, and nsid directly.
A malformed or forged SQ entry can do the same.

This leads to two memory bugs:

  • Out-of-bounds SLBA or length
    • A large slba makes ns->mapped + offset point outside the backing store.
    • The memcpy is then a wild read or write.
    • NSID 0 also makes nsid - 1 a huge index into nvmev_vdev->ns[].
  • Bad PRP
    • If a PRP address is not mapped, memremap() returns NULL.
    • The next memcpy then dereferences NULL.
    • This happens when the transfer length is larger than the mapped data buffer, so NVMeVirt walks PRP entries that were never set.

Description

With KASAN enabled, the out-of-bounds access faults in the I/O worker.

This was reproduced on unpatched Linux 7.1.0 by reading far past the namespace size.
The namespace is 1023 MiB, and the read starts at LBA 2^30 (offset 512 GiB).

Oops: 0000 [#1] SMP KASAN NOPTI
CPU: 3 UID: 0 PID: 303 Comm: nvmev_io_worker Tainted: G           O        7.1.0
RIP: 0010:kasan_check_range+0xb5/0x1b0
  nvmev_io_worker+0x1780/0x2000 [nvmev]
Kernel panic - not syncing: Fatal exception

The bad-PRP case instead shows KASAN: null-ptr-deref Write in nvmev_io_worker.

This bug was found by Syzkaller during fuzzing.

Linux 7.1.0 x86_64, CONFIG_KASAN=y, panic_on_warn=1, kasan.fault=panic, running under QEMU/KVM.

Reproduction

On the test machine, with NVMeVirt probed as /dev/nvme0n1.
Pick a start block well past the namespace size.
See io-range-repro.sh:

# OOB SLBA: read 1 block starting at LBA 2^30 (offset 512 GiB).
# ns->mapped + offset lands far outside the backing store (wild read).
# `nvme read` uses NVME_IOCTL_SUBMIT_IO; the kernel forwards slba/nblocks as-is.
nvme read /dev/nvme0n1 --start-block=1073741824 --block-count=0 \
     --data-size=512 > /dev/null

For the bad-PRP case, send an I/O passthrough whose transfer length is larger than the data buffer.
bug-C-repro.sh has an example.

Fix

In __do_perform_io():

  • Reject an I/O whose [offset, offset+length) range is outside the namespace size (and whose nsid is out of range) before any transfer.
  • NULL-check every resolved PRP (the PRP-list page and each data page) and stop the transfer instead of dereferencing NULL.

A fully spec-faithful version would also return NVME_SC_LBA_RANGE to the host.
This patch focuses on the memory-safety hole.

Validated on Linux 7.1.0.

__do_perform_io() builds the backing-store address directly from the
host-supplied SLBA / transfer length and from the PRP entries, with no
validation:

  offset = (cmd->slba) << LBA_BITS;          // host SLBA
  length = (cmd->length + 1) << LBA_BITS;     // host transfer length
  ...
  memcpy(nvmev_vdev->ns[nsid].mapped + offset, vaddr + mem_offs, io_size);

A normal block-layer read/write is bounded by the disk capacity, so this
never fires in ordinary use. But a passthrough I/O command lets user space
set slba/nblocks (and nsid) directly, and a malformed/forged SQ entry can
do the same. Two memory-safety bugs result:

  1. A large SLBA/length makes ns->mapped + offset point outside the
     mapped backing store -> wild read/write (GPF, or silent corruption
     of unrelated kernel memory):

       KASAN: wild-memory-access Read in nvmev_io_worker
       general protection fault in nvmev_io_worker

  2. A PRP whose physical address is not mapped makes memremap() return
     NULL (after a WARNING in __ioremap_caller); the following memcpy then
     dereferences NULL:

       WARNING in __ioremap_caller
       KASAN: null-ptr-deref Write in nvmev_io_worker

Fix both:
  - Reject an I/O whose [offset, offset+length) range falls outside the
    namespace size (and whose nsid is out of range) before any transfer.
  - NULL-check every resolved PRP -- both the PRP-list page and each data
    page -- and abort the transfer instead of dereferencing NULL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant