DragonFlyBSD Kernel Audit
DF-0002 / fhopen_spur.c
← back to finding ↓ download raw
/*
 * 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;
}