DF-0002 / fhopen_spur.c
/* * DF-0002 PoC - demonstrates sys_fhopen() returning spurious success * (fd 0 / stdin) without setting an error when a VREG vnode has no VM * object after VOP_OPEN (vfs_syscalls.c:4933-4938 missing `error = EINVAL`). * * This PoC mounts a tmpfs, creates a regular file, opens it, and gets a * file handle. It then asks the df0002_trigger KLD to null that vnode's * v_object (simulating the FS-invariant violation the buggy branch is * guarding against), and finally calls fhopen(2) on the handle. Because * tmpfs_open() does NOT re-create v_object, sys_fhopen's v_object==NULL * check fires and the bug returns spurious success. * * Root-only correctness demonstration (fhopen requires SYSCAP_RESTRICTEDROOT). * No security impact: there is no privilege boundary being crossed. * * Build: cc -o fhopen_spur fhopen_spur.c * Run: ./fhopen_spur (after kldload df0002_trigger) * * Expected (bug present): prints * fhopen returned 0 (success) * BUG: fd 0 is stdin, no real descriptor was allocated * Expected (fixed): prints * fhopen failed as expected: Invalid argument (errno=22) */ #include <sys/param.h> #include <sys/mount.h> #include <sys/sysctl.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <stdlib.h> #include <err.h> #define TMPMNT "/tmp/df0002_tmpfs" #define TMPFILE TMPMNT "/file" int main(void) { struct fhandle fh; int mfd, fd, sysctl_fd; int mib[CTL_MAXNAME]; size_t miblen; char fstype[] = "tmpfs"; /* 0. resolve the debug.df0002.corrupt_fd OID early so we can also use * it to restore any prior corruption before we open the file. */ miblen = sizeof(mib) / sizeof(int); if (sysctlnametomib("debug.df0002.corrupt_fd", mib, &miblen) != 0) err(1, "sysctlnametomib debug.df0002.corrupt_fd (is df0002_trigger.ko loaded?)"); sysctl_fd = -1; /* restore-only */ sysctl(mib, miblen, NULL, NULL, &sysctl_fd, sizeof(sysctl_fd)); /* 1. mount a tmpfs we control (tmpfs_mount accepts data=NULL). */ if (mkdir(TMPMNT, 0755) < 0 && errno != EEXIST) err(1, "mkdir %s", TMPMNT); if (mount(fstype, TMPMNT, 0, NULL) < 0 && errno != EBUSY) err(1, "mount tmpfs at %s", TMPMNT); /* 2. create + open a regular file in the tmpfs. */ mfd = open(TMPFILE, O_RDWR | O_CREAT, 0644); if (mfd < 0) err(1, "open %s", TMPFILE); if (write(mfd, "hello", 5) != 5) err(1, "write"); /* 3. get the file handle BEFORE we corrupt the vnode. */ if (getfh(TMPFILE, &fh) != 0) err(1, "getfh %s", TMPFILE); /* 4. ask the KLD to null v_object on mfd's vnode. */ sysctl_fd = mfd; if (sysctl(mib, miblen, NULL, NULL, &sysctl_fd, sizeof(sysctl_fd)) != 0) err(1, "sysctl debug.df0002.corrupt_fd=%d", sysctl_fd); printf("[*] v_object nullified on vnode for fd %d (%s)\n", mfd, TMPFILE); /* 5. close the file we held; the vnode stays referenced by the mount * and the KLD's extra reference. */ close(mfd); /* 6. fhopen(). Because v_object is now NULL on that vnode, sys_fhopen * triggers the buggy branch at vfs_syscalls.c:4933-4938. */ errno = 0; fd = fhopen(&fh, O_RDWR); if (fd >= 0) { printf("fhopen returned %d (success)\n", fd); if (fd == 0) printf("BUG: fd 0 is stdin, no real descriptor was allocated\n"); else printf("NOTE: fd %d was returned (unexpected; bug expected fd=0)\n", fd); return 0; } printf("fhopen failed as expected: %s (errno=%d)\n", strerror(errno), errno); return 0; } |