DragonFlyBSD Kernel Audit
DF-0002 / df0002_trigger.c
← back to finding ↓ download raw
/*
 * DF-0002 trigger module.
 *
 * sys_fhopen() at sys/kern/vfs_syscalls.c:4933-4938 has a missing
 * `error = EINVAL` before `goto bad_drop` when a VREG vnode has v_object
 * == NULL after VOP_OPEN. The function returns 0 (success) while the
 * reserved fd slot is freed and sysmsg_result is never written, so the
 * caller sees "fd 0" (its stdin).
 *
 * No in-tree filesystem produces a VREG vnode with v_object == NULL on a
 * successful VOP_OPEN (UFS/HAMMER2/tmpfs/cd9660/msdosfs/etc. all call
 * vinitvmio() up front). The bug is therefore unreachable on default
 * filesystems; it requires an out-of-tree/broken FS that violates the
 * invariant. To demonstrate the bug we artificially recreate that
 * invariant violation.
 *
 * This KLD exposes a sysctl that takes an open fd for a tmpfs regular file.
 * It looks up the vnode, saves and NULLs v_object, and prints a marker.
 * The caller then issues fhopen(2) on a handle obtained earlier via
 * getfh(2); because tmpfs_open() (unlike ufs_open) does NOT re-create
 * v_object, sys_fhopen's VOP_OPEN succeeds but vp->v_object stays NULL and
 * the buggy branch at vfs_syscalls.c:4933 fires.
 *
 * Root-only by definition (kldload). The bug itself is also gated by
 * SYSCAP_RESTRICTEDROOT, so this is not a privilege escalation -- it is a
 * logic/correctness demonstration. There is no security impact.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/proc.h>
#include <sys/filedesc.h>
#include <sys/file.h>
#include <sys/vnode.h>
#include <sys/mount.h>
#include <vm/vm_object.h>

static struct vnode *corrupted_vp = NULL;
static vm_object_t  saved_vobj = NULL;

static int
df0002_corrupt_fd(SYSCTL_HANDLER_ARGS)
{
	int fd = -1;
	int error;
	struct file *fp = NULL;
	struct vnode *vp;

	error = sysctl_handle_int(oidp, &fd, 0, req);
	if (error != 0 || req->newptr == NULL)
		return (error);

	/* Restore any previously corrupted vnode before corrupting a new one. */
	if (corrupted_vp != NULL) {
		struct vnode *ovp = corrupted_vp;
		corrupted_vp = NULL;
		vn_lock(ovp, LK_EXCLUSIVE | LK_RETRY);
		ovp->v_object = saved_vobj;
		saved_vobj = NULL;
		vput(ovp);
	}

	if (fd < 0)
		return (0);

	error = holdvnode(curthread, fd, &fp);
	if (error) {
		kprintf("DF-0002: holdvnode(%d) failed: %d\n", fd, error);
		return (error);
	}
	vp = (struct vnode *)fp->f_data;
	if (vp == NULL || vp->v_type != VREG) {
		kprintf("DF-0002: fd %d is not a regular file (type=%d)\n",
		    fd, vp ? vp->v_type : -1);
		fdrop(fp);
		return (EINVAL);
	}

	/* Hold an extra reference on the vnode so it survives the fdrop(). */
	vn_lock(vp, LK_EXCLUSIVE | LK_RETRY);
	saved_vobj = vp->v_object;
	vp->v_object = NULL;
	vget(vp, LK_EXCLUSIVE | LK_RETRY);	/* take extra ref for corrupted_vp */
	vput(vp);				/* release the lock from vn_lock */
	corrupted_vp = vp;
	fdrop(fp);

	kprintf("DF-0002: corrupted vnode %p (cleared v_object, was %p, type=VREG)\n",
	    vp, saved_vobj);
	return (0);
}

SYSCTL_NODE(_debug, OID_AUTO, df0002, CTLFLAG_RW, 0, "DF-0002 trigger");
SYSCTL_PROC(_debug_df0002, OID_AUTO, corrupt_fd, CTLTYPE_INT | CTLFLAG_RW,
    NULL, 0, df0002_corrupt_fd, "I",
    "Open fd whose vnode's v_object should be nulled to trigger the bug");

static int
df0002_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		kprintf("DF-0002: trigger module loaded. sysctl: df0002.corrupt_fd\n");
		return (0);
	case MOD_UNLOAD:
		if (corrupted_vp != NULL) {
			struct vnode *ovp = corrupted_vp;
			corrupted_vp = NULL;
			vn_lock(ovp, LK_EXCLUSIVE | LK_RETRY);
			ovp->v_object = saved_vobj;
			saved_vobj = NULL;
			vput(ovp);
		}
		kprintf("DF-0002: trigger module unloaded\n");
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df0002_moddata = {
	"df0002_trigger",
	df0002_modevent,
	NULL
};

DECLARE_MODULE(df0002_trigger, df0002_moddata, SI_SUB_PSEUDO, SI_ORDER_ANY);
MODULE_VERSION(df0002_trigger, 1);