nvme_query_fdp_granularity() parses the FDP Configurations log using sizes the controller returns, without checking that they fit the buffer it was given. The log size is only bounded from above before kvmalloc(), so a size below the log header makes le16_to_cpu(h->numfdpc) read past the allocation, and a size of zero yields ZERO_SIZE_PTR and faults. Each configuration descriptor is then dereferenced (dsze while walking, and nrg and runs on the selected one) without checking that the whole descriptor lies within the buffer, so a descriptor at or past the end is read out of bounds. The out-of-bounds runs value is then exposed to userspace through /sys/block/nvmeXnY/queue/write_stream_granularity. A device-supplied dsze that is not a multiple of eight also leaves the following descriptors unaligned, and the structure is not packed, so reading nrg and runs from one can fault on architectures without efficient unaligned access. A malicious or buggy controller triggers this during namespace scan with a short FDP Configurations log: [ 1.104828] BUG: KASAN: slab-out-of-bounds in nvme_alloc_ns+0x39a6/0x4480 [ 1.104848] Read of size 8 at addr ffff888009fd8fe0 by task kworker/u8:3/52 [ 1.104852] [ 1.104857] CPU: 1 UID: 0 PID: 52 Comm: kworker/u8:3 Not tainted 7.3.0-rc6-00063-g0c2669a9f4a1 #1 PREEMPT(lazy) [ 1.104862] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS rel-1.17.0-0-gb52ca86e094d-prebuilt.qemu.org 04/01/2014 [ 1.104866] Workqueue: async async_run_entry_fn [ 1.104873] Call Trace: [ 1.104876] [ 1.104878] dump_stack_lvl+0x66/0xa0 [ 1.104886] print_report+0xd0/0x630 [ 1.104893] ? nvme_alloc_ns+0x39a6/0x4480 [ 1.104897] ? __virt_addr_valid+0x209/0x3f0 [ 1.104903] ? nvme_alloc_ns+0x39a6/0x4480 [ 1.104907] kasan_report+0xe4/0x120 [ 1.104913] ? nvme_alloc_ns+0x39a6/0x4480 [ 1.104919] nvme_alloc_ns+0x39a6/0x4480 [ 1.104926] ? __pfx_nvme_alloc_ns+0x10/0x10 [ 1.104931] ? check_prev_add+0xfd/0xe80 [ 1.104942] ? lock_acquire+0x18d/0x300 [ 1.104948] ? nvme_find_get_ns+0xbb/0x2e0 [ 1.104952] ? find_held_lock+0x2b/0x80 [ 1.104956] ? nvme_find_get_ns+0x221/0x2e0 [ 1.104962] ? nvme_find_get_ns+0x231/0x2e0 [ 1.104967] ? nvme_scan_ns+0x2e1/0x8d0 [ 1.104971] ? kfree+0x2e5/0x510 [ 1.104976] ? __lock_acquire+0x570/0x1b00 [ 1.104982] nvme_scan_ns+0x56a/0x8d0 [ 1.104987] ? __pfx_nvme_scan_ns+0x10/0x10 [ 1.104994] ? lockdep_hardirqs_on_prepare+0xdc/0x190 [ 1.104999] ? trace_hardirqs_on+0x18/0x160 [ 1.105005] ? kvm_clock_get_cycles+0x19/0x40 [ 1.105011] ? __pfx_nvme_scan_ns_async+0x10/0x10 [ 1.105015] async_run_entry_fn+0x8d/0x290 [ 1.105020] process_one_work+0x8ac/0x1b00 [ 1.105029] ? __pfx_process_one_work+0x10/0x10 [ 1.105034] ? lock_acquire+0x18d/0x300 [ 1.105040] ? lock_is_held_type+0x8f/0x100 [ 1.105046] ? __pfx_async_run_entry_fn+0x10/0x10 [ 1.105050] worker_thread+0x4ee/0xe70 [ 1.105056] ? lockdep_hardirqs_on_prepare+0xdc/0x190 [ 1.105061] ? trace_hardirqs_on+0x18/0x160 [ 1.105066] ? __pfx_worker_thread+0x10/0x10 [ 1.105071] ? __pfx_worker_thread+0x10/0x10 [ 1.105076] kthread+0x2ce/0x3b0 [ 1.105080] ? __pfx_kthread+0x10/0x10 [ 1.105085] ret_from_fork+0x52e/0x780 [ 1.105090] ? __pfx_ret_from_fork+0x10/0x10 [ 1.105094] ? __switch_to+0x572/0xde0 [ 1.105101] ? __pfx_kthread+0x10/0x10 [ 1.105105] ret_from_fork_asm+0x1a/0x30 [ 1.105114] [ 1.105116] [ 1.105117] Allocated by task 52: [ 1.105119] kasan_save_stack+0x33/0x60 [ 1.105123] kasan_save_track+0x14/0x30 [ 1.105126] __kasan_kmalloc+0x8f/0xa0 [ 1.105129] __kvmalloc_node_noprof+0x2e1/0x820 [ 1.105134] nvme_alloc_ns+0x1097/0x4480 [ 1.105138] nvme_scan_ns+0x56a/0x8d0 [ 1.105142] async_run_entry_fn+0x8d/0x290 [ 1.105145] process_one_work+0x8ac/0x1b00 [ 1.105148] worker_thread+0x4ee/0xe70 [ 1.105152] kthread+0x2ce/0x3b0 [ 1.105155] ret_from_fork+0x52e/0x780 [ 1.105159] ret_from_fork_asm+0x1a/0x30 [ 1.105163] [ 1.105164] The buggy address belongs to the object at ffff888009fd8fc0 [ 1.105164] which belongs to the cache kmalloc-32 of size 32 [ 1.105167] The buggy address is located 8 bytes to the right of [ 1.105167] allocated 24-byte region [ffff888009fd8fc0, ffff888009fd8fd8) [ 1.105171] [ 1.105172] The buggy address belongs to the physical page: [ 1.105175] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x9fd8 [ 1.105179] flags: 0x100000000000000(node=0|zone=1) [ 1.105183] page_type: f5(slab) [ 1.105188] raw: 0100000000000000 ffff888008c41780 dead000000000100 dead000000000122 [ 1.105192] raw: 0000000000000000 0000000000400040 00000000f5000000 0000000000000000 [ 1.105194] page dumped because: kasan: bad access detected [ 1.105195] [ 1.105196] Memory state around the buggy address: [ 1.105198] ffff888009fd8e80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 1.105201] ffff888009fd8f00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 1.105203] >ffff888009fd8f80: fc fc fc fc fc fc fc fc 00 00 00 fc fc fc fc fc [ 1.105205] ^ [ 1.105207] ffff888009fd9000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fc [ 1.105210] ffff888009fd9080: fc fc fc fc fc fc fc fc 00 00 00 00 00 00 00 00 [ 1.105212] ================================================================== Reject a log smaller than its header plus one configuration descriptor, zero the buffer so a short transfer leaves no uninitialized tail to parse, require each descriptor's declared size to be at least a descriptor and to leave room for the next one before walking it, and read the descriptor fields with the unaligned accessors. Fixes: 30b5f20bb2dda ("nvme: register fdp parameters with the block layer") Cc: stable@vger.kernel.org Suggested-by: Kanchan Joshi Signed-off-by: Yehyeong Lee --- v4: per Kanchan's review - require header plus one descriptor, fold the per-descriptor bounds check into the walk (reject dsze < sizeof(desc), covering the 1..63 values the old !dsze test allowed, and require the next descriptor to fit). v3: read the descriptor fields with get_unaligned_le() (strict-align hardening; verified via codegen, not on strict-align HW). v2: allocate the log buffer with kvzalloc() so a short Get Log Page transfer leaves a zeroed, rejected tail. v1: https://lore.kernel.org/linux-nvme/20261008132604.1168105-1-yhlee@isslab.korea.ac.kr/ drivers/nvme/host/core.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 9bcab3dc4c118..389ef1ae53f17 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -2272,13 +2272,18 @@ static int nvme_query_fdp_granularity(struct nvme_ctrl *ctrl, } size = le32_to_cpu(hdr.sze); + if (size < sizeof(*h) + sizeof(*desc)) { + dev_warn(ctrl->device, "FDP config size too small:%zu\n", + size); + return 0; + } if (size > PAGE_SIZE * MAX_ORDER_NR_PAGES) { dev_warn(ctrl->device, "FDP config size too large:%zu\n", size); return 0; } - h = kvmalloc(size, GFP_KERNEL); + h = kvzalloc(size, GFP_KERNEL); if (!h) return -ENOMEM; @@ -2304,9 +2309,9 @@ static int nvme_query_fdp_granularity(struct nvme_ctrl *ctrl, desc = log; end = log + size - sizeof(*h); for (i = 0; i < fdp_idx; i++) { - u16 dsze = le16_to_cpu(desc->dsze); + u16 dsze = get_unaligned_le16(&desc->dsze); - if (!dsze || log + dsze > end) { + if (dsze < sizeof(*desc) || log + dsze + sizeof(*desc) > end) { dev_warn(ctrl->device, "FDP invalid config descriptor at index %d\n", i); ret = 0; @@ -2316,13 +2321,13 @@ static int nvme_query_fdp_granularity(struct nvme_ctrl *ctrl, desc = log; } - if (le32_to_cpu(desc->nrg) > 1) { + if (get_unaligned_le32(&desc->nrg) > 1) { dev_warn(ctrl->device, "FDP NRG > 1 not supported\n"); ret = 0; goto out; } - info->runs = le64_to_cpu(desc->runs); + info->runs = get_unaligned_le64(&desc->runs); out: kvfree(h); return ret; base-commit: 0c2669a9f4a1d607e7591ae50ccf3c432a0aff08 -- 2.43.0